Skip to content

Standard validation rules#

Protovalidate's built-in rules cover most validation needs—from string length to email formats to numeric ranges—with easy-to-read, intuitive annotations that you add directly to your schemas.

Fundamentals#

Protovalidate follows these core principles:

Rules are composable#

You can combine multiple rules on a single field. All rules must pass for validation to succeed:

message User {
  string email = 1 [
    (buf.validate.field).string.email = true,
    (buf.validate.field).string.max_len = 254,
    (buf.validate.field).string.min_len = 3
  ];
}

Explicit presence skips validation#

If a field has explicit presence (proto2 scalars, proto3 optional, messages, oneofs) and isn't set, Protovalidate skips all validation rules for that field, unless you use the required rule.

message Product {
  // If `description` is not set, min_len is not enforced
  optional string description = 1 [
    (buf.validate.field).string.min_len = 10
  ];

  // If `name` is not set, validation fails due to `required`
  optional string name = 2 [
    (buf.validate.field).required = true,
    (buf.validate.field).string.min_len = 1
  ];
}

See Required fields for more details on field presence.

Implicit presence always validates#

Fields without explicit presence (proto3 scalars without optional, repeated, map) are always validated, even when they have their default value:

message Config {
  // This field is always validated, even if never set (defaults to empty string)
  string hostname = 1 [
    (buf.validate.field).string.min_len = 1  // Will fail if not set
  ];

  // This repeated field is always validated, even if never set (defaults to empty list)
  repeated string tags = 2 [
    (buf.validate.field).repeated.min_items = 1  // Will fail if not set
  ];
}

See Required fields for more details on field presence.

Nested messages are validated#

Nested messages are automatically validated. If a parent message is valid but contains an invalid nested message, the parent message fails validation:

message Order {
  // If any LineItem violates its rules, the entire Order is invalid
  repeated LineItem items = 1;
}

message LineItem {
  string product_id = 1 [(buf.validate.field).string.min_len = 1];
  uint32 quantity = 2 [(buf.validate.field).uint32.gt = 0];
}

Use ignore to skip validation of specific nested messages. See Ignore rules.

Failures are structured#

Validation failures return structured violation objects, not plain strings. Each violation includes:

  • The field path
  • The constraint that failed (e.g., string.min_len)
  • A human-readable message
  • The rule ID (for custom CEL rules)

This structure makes it easy to map errors back to specific fields in your UI or API responses.

Rules are only annotations#

Validation rules are annotations in your .proto files. They don't appear in the wire format, don't affect message size, and don't impact serialization or deserialization performance. Validation only happens when you explicitly call a validation function in your code.

Scalar validation#

Scalar fields—strings, numbers, bytes, and bools—are the most common fields you'll validate. Protovalidate's built-in rules handle everything from string length to email formats and regular expressions.

Length#

Use min_len and max_len to enforce length constraints on strings and bytes. These rules also work with google.protobuf.StringValue and google.protobuf.BytesValue. For strings, these count characters (Unicode code points). Use min_bytes and max_bytes to count bytes instead.

message Post {
  // Character limits on strings
  string title = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 100
  }];

  // Byte limits on strings (useful for database columns or transmission size)
  string body = 2 [(buf.validate.field).string.max_bytes = 65536];

  // Byte count limits on bytes
  bytes thumbnail = 3 [(buf.validate.field).bytes.max_len = 10240];
}

Try it in the Playground ↗

Exact length#

Use len to require an exact length. For strings, len counts characters (Unicode code points). Use len_bytes to count bytes instead.

message Code {
  // Exactly 2 characters
  string country_code = 1 [(buf.validate.field).string.len = 2];
  // Exactly 32 bytes
  string api_key = 2 [(buf.validate.field).string.len_bytes = 32];
  // Exactly 32 bytes
  bytes hash = 3 [(buf.validate.field).bytes.len = 32];
}

Try it in the Playground ↗

Numeric ranges#

Use gt (greater than), gte (greater than or equal), lt (less than), and lte (less than or equal) to validate numeric values. These work the same across all numeric types—int32, int64, uint32, float, double, and their corresponding Well-Known-Types like google.protobuf.Int32Value.

For float and double fields, use finite to prevent infinity and NaN.

message Product {
  int32 quantity = 1 [(buf.validate.field).int32 = {
    gte: 0
    lte: 1000
  }];
  float price = 2 [(buf.validate.field).float.gt = 0];
  // For floats and doubles, prevent infinity and NaN
  double score = 3 [(buf.validate.field).double.finite = true];
}

Try it in the Playground ↗

Sets of values#

Use in to restrict values to a specific set. Use not_in to exclude values instead. Available for strings, bytes, and all numeric types.

message Region {
  string country_code = 1 [(buf.validate.field).string = {
    in: ["USA", "CAN", "MEX"]
  }];
  bytes magic_number = 2 [(buf.validate.field).bytes = {
    in: ["\x89\x50\x4E\x47", "\xFF\xD8\xFF"]
  }];
}

Try it in the Playground ↗

Constant/exact values#

Use const to require a field to match an exact value. Available for all scalar types:

message Config {
  string environment = 1 [(buf.validate.field).string.const = "production"];
  int32 version = 2 [(buf.validate.field).int32.const = 2];
  bytes signature = 3 [(buf.validate.field).bytes.const = "\x01\x02\x03\x04"];
}

Try it in the Playground ↗

Prefix, suffix, and contains#

Use prefix, suffix, and contains to validate strings and byte sequences. not_contains is also available for strings.

message File {
  string name = 1 [(buf.validate.field).string.suffix = ".pdf"];
  string path = 2 [(buf.validate.field).string.prefix = "/uploads/"];
  // PNG file header
  bytes header = 3 [(buf.validate.field).bytes.prefix = "\x89\x50\x4E\x47"];
  string content = 4 [(buf.validate.field).string.contains = "signature"];
  string metadata = 5 [(buf.validate.field).string.not_contains = "confidential"];
}

Try it in the Playground ↗

Regular expressions#

Use pattern with a regular expression (RE2 syntax) for custom string or bytes validation. If used with bytes, field values must be valid UTF-8.

message Order {
  // Order IDs are always ORD- followed by 8 digits
  string order_id = 1 [(buf.validate.field).string.pattern = "^ORD-[0-9]{8}$"];
}

Try it in the Playground ↗

Email addresses#

The email rule ensures the string is a valid email address format according to the the HTML standard. This intentionally deviates from RFC 5322, which allows many unexpected forms of email addresses and easily matches typographical errors.

message User {
  string email = 1 [(buf.validate.field).string.email = true];
}

Try it in the Playground ↗

UUIDs and TUUIDs#

Use uuid to validate standard UUID format, or tuuid for UUIDs without dashes:

message Resource {
  // UUID: "123e4567-e89b-12d3-a456-426614174000"
  string id = 1 [(buf.validate.field).string.uuid = true];
  // TUUID: "123e4567e89b12d3a456426614174000"
  string compact_id = 2 [(buf.validate.field).string.tuuid = true];
}

Try it in the Playground ↗

Hostnames and URIs#

The hostname and uri rules validate hostnames and URIs, respectively. Use uri_ref to also allow relative references:

message Config {
  string hostname = 1 [(buf.validate.field).string.hostname = true];
  // Absolute URIs only: "https://example.com/path"
  string api_endpoint = 2 [(buf.validate.field).string.uri = true];
  // URIs or relative reference: "https://example.com/path" or "./path"
  string redirect = 3 [(buf.validate.field).string.uri_ref = true];
}

Try it in the Playground ↗

Network validation#

Protovalidate includes validation rules for IP addresses, network prefixes, and related formats:

message Network {
  // IP addresses (any version or specific)
  string ip = 1 [(buf.validate.field).string.ip = true];
  string ipv4 = 2 [(buf.validate.field).string.ipv4 = true];
  string ipv6 = 3 [(buf.validate.field).string.ipv6 = true];

  // IP with CIDR notation: "192.168.1.0/24" or "2001:db8::/32"
  string subnet = 4 [(buf.validate.field).string.ip_with_prefixlen = true];

  // Network prefixes (strict - host bits must be zero)
  string network = 5 [(buf.validate.field).string.ip_prefix = true];

  // Hostname or IP address
  string server = 6 [(buf.validate.field).string.address = true];

  // Host and port combinations: "example.com:8080" or "[::1]:8080"
  string endpoint = 7 [(buf.validate.field).string.host_and_port = true];
}

Try it in the Playground ↗

The ip, ipv4, ipv6 rules are also available for bytes.

For IPv4-specific or IPv6-specific CIDR and prefix validation, use ipv4_with_prefixlen, ipv6_with_prefixlen, ipv4_prefix, or ipv6_prefix.

HTTP headers#

Use well_known_regex for HTTP header validation:

message Request {
  string header_name = 1 [(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_NAME];
  string header_value = 2 [(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_VALUE];
}

Try it in the Playground ↗

The default expressions follow RFC 7230. Set strict = false to allow any values except \r\n\0:

message LenientRequest {
  string header_name = 1 [
    (buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_NAME,
    (buf.validate.field).string.strict = false
  ];
}

Example values#

Use example to document valid values for a field. These don't affect validation but help consumers understand what values are expected. Available for all scalar types:

message User {
  string username = 1 [
    (buf.validate.field).string.example = "alice",
    (buf.validate.field).string.example = "bob"
  ];
  int32 age = 2 [
    (buf.validate.field).int32.example = 25,
    (buf.validate.field).int32.example = 40
  ];
  bytes token = 3 [
    (buf.validate.field).bytes.example = "\x01\x02",
    (buf.validate.field).bytes.example = "\x02\x03"
  ];
}

Reference#

For the complete list of scalar field rules:

Durations#

Duration rules apply to google.protobuf.Duration fields. Use duration rules to validate time spans like timeouts, delays, or intervals.

Duration ranges#

Use gt, gte, lt, and lte to validate durations:

message Config {
  // Timeout must be at least 1 second
  google.protobuf.Duration timeout = 1 [(buf.validate.field).duration.gte = {seconds: 1}];

  // Retry delay must be less than 30 seconds
  google.protobuf.Duration retry_delay = 2 [(buf.validate.field).duration.lt = {seconds: 30}];

  // Cache TTL must be between 1 minute and 1 hour
  google.protobuf.Duration cache_ttl = 3 [(buf.validate.field).duration = {
    gte: {seconds: 60}
    lte: {seconds: 3600}
  }];
}

Try it in the Playground ↗

Exact duration#

Use const to require an exact duration:

message Schedule {
  // Interval must be exactly 5 minutes
  google.protobuf.Duration interval = 1 [(buf.validate.field).duration.const = {seconds: 300}];
}

Try it in the Playground ↗

Sets of durations#

Use in to restrict values to specific durations, or not_in to exclude values:

message RateLimit {
  // Window must be 1m, 5m, or 15m
  google.protobuf.Duration window = 1 [(buf.validate.field).duration = {
    in: [
        {seconds: 60},
        {seconds: 300},
        {seconds: 900}
    ]
  }];
}

Try it in the Playground ↗

Reference#

Timestamps#

Timestamp rules apply to google.protobuf.Timestamp fields. Use timestamp rules to validate dates, times, and temporal constraints.

Timestamp ranges#

Use gt, gte, lt, and lte to validate timestamps against specific points in time, expressed as seconds and nanos of time since Unix epoch.

message Event {
  // Must be after 2024-01-01
  google.protobuf.Timestamp scheduled_at = 1 [(buf.validate.field).timestamp.gt = {seconds: 1704067200}];

  // Must be before 2025-01-01 (exclusive)
  google.protobuf.Timestamp deadline = 2 [(buf.validate.field).timestamp.lt = {seconds: 1735689600}];

  // Must be within 2024
  google.protobuf.Timestamp occurred_at = 3 [(buf.validate.field).timestamp = {
    gte: {seconds: 1704067200}
    lt: {seconds: 1735689600}
  }];
}

Try it in the Playground ↗

Relative to current time#

Use lt_now and gt_now to validate timestamps relative to the current time:

message Audit {
  // Created timestamp must be in the past
  google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true];

  // Expiration must be in the future
  google.protobuf.Timestamp expires_at = 2 [(buf.validate.field).timestamp.gt_now = true];
}

Try it in the Playground ↗

Time windows#

Use within with lt_now or gt_now to validate timestamps within a duration of the current time:

message Session {
  // Must have been created within the past 24 hours
  google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp = {
    lt_now: true
    within: {seconds: 84600}
  }];

  // Must expire within the next 24 hours
  google.protobuf.Timestamp expires_at = 2 [(buf.validate.field).timestamp = {
    gt_now: true
    within: {seconds: 86400}
  }];
}

Try it in the Playground ↗

Exact timestamp#

Use const to require an exact timestamp. Use seconds and nanos of time since Unix epoch.

message Milestone {
  // Launch date must be exactly 2024-06-01T00:00:00Z
  google.protobuf.Timestamp launch_date = 1 [(buf.validate.field).timestamp.const = {seconds: 1717200000}];
}

Try it in the Playground ↗

Reference#

Any fields#

Any rules apply to google.protobuf.Any fields, which can hold any message type. Use Any rules to restrict which types are allowed.

Restrict type URLs#

Use in to allow only specific message types, or not_in to exclude types:

message Container {
  // Only allow specific types
  google.protobuf.Any payload = 1 [(buf.validate.field).any = {
    in: [
        "type.googleapis.com/playground.UserEvent",
        "type.googleapis.com/playground.SystemEvent"
    ]
  }];


  // Exclude sensitive types
  google.protobuf.Any metadata = 2 [(buf.validate.field).any = {
    not_in: [
      "type.googleapis.com/playground.SecretData",
      "type.googleapis.com/playground.PrivateInfo"
    ]
  }];
}

Try it in the Playground ↗

Reference#

Repeated and map fields#

Protovalidate's repeated and map field rules start simply, with size, but let you dive all the way into checking collections of nested messages against your business rules.

Size#

Use min_items and max_items for repeated fields, or min_pairs and max_pairs for maps:

message Team {
  // Must have at least one member
  repeated string members = 1 [(buf.validate.field).repeated.min_items = 1];

  // No more than 100 tags
  repeated string tags = 2 [(buf.validate.field).repeated.max_items = 100];

  // At least one configuration entry required
  map<string, string> settings = 3 [(buf.validate.field).map.min_pairs = 1];
}

Try it in the Playground ↗

Validate items#

Use items to apply validation rules to each element in a repeated field:

message Article {
  // All email addresses must be valid
  repeated string authors = 1 [(buf.validate.field).repeated.items.string.email = true];

  // All tags must be between 1 and 50 characters
  repeated string tags = 2 [(buf.validate.field).repeated.items.string = {
    min_len: 1
    max_len: 50
  }];
}

Try it in the Playground ↗

Unique items#

Use unique to ensure all items in a repeated field are unique. Works for scalar and enum types only:

message Permissions {
  // No duplicate user IDs
  repeated string user_ids = 1 [(buf.validate.field).repeated.unique = true];
}

Try it in the Playground ↗

Validate keys and values#

Use keys and values to apply validation rules to map entries:

message Metadata {
  // Keys must be valid identifiers, values must be non-empty
  map<string, string> labels = 1 [
    (buf.validate.field).map.keys.string.pattern = "^[a-z0-9]+$",
    (buf.validate.field).map.values.string.min_len = 1
  ];
}

Try it in the Playground ↗

Nested messages#

CEL lets you access fields within nested messages. With functions like all(), exists(), and filter(), you can start expressing more complex business rules right within your schema:

message LineItem {
  string product_id = 1;
  int32 quantity = 2;
  bool in_stock = 3;
}

message BulkOrder {
  repeated LineItem items = 1;
  bool allow_backorder = 2;

  // If backorders not allowed, all items must be in stock
  option (buf.validate.message).cel = {
    id: "order.stock_check"
    message: "when backorders disabled, all items must be in stock"
    expression: "this.allow_backorder || this.items.all(i, i.in_stock)"
  };

  // Must have at least one item with quantity > 10
  option (buf.validate.message).cel = {
    id: "order.has_bulk"
    message: "order must contain at least one bulk item (quantity > 10)"
    expression: "this.items.exists(i, i.quantity > 10)"
  };
}

Try it in the Playground ↗

Learn more about CEL in custom rules.

Reference#

Enums#

Enum validation ensures fields contain valid enum values and can restrict which values are allowed.

Reject undefined values#

Use defined_only to reject any value not defined in the enum:

enum Status {
  STATUS_UNSPECIFIED = 0;
  STATUS_PENDING = 1;
  STATUS_ACTIVE = 2;
  STATUS_ARCHIVED = 3;
}

message Order {
  Status status = 1 [(buf.validate.field).enum.defined_only = true];
}

Try it in the Playground ↗

Restrict to specific values#

Use in to allow only specific enum values, or not_in to exclude values:

enum Priority {
  PRIORITY_UNSPECIFIED = 0;
  PRIORITY_LOW = 1;
  PRIORITY_MEDIUM = 2;
  PRIORITY_HIGH = 3;
  PRIORITY_CRITICAL = 4;
}

message Task {
  // Only allow low or medium priority
  Priority priority = 1 [
    (buf.validate.field).enum = { in: [1, 2] },
    (buf.validate.field).enum.example = 1,  // PRIORITY_LOW
    (buf.validate.field).enum.example = 2   // PRIORITY_MEDIUM
  ];

  // Don't allow unspecified
  Priority backup_priority = 2 [(buf.validate.field).enum = {
    not_in: [0]
  }];
}

Try it in the Playground ↗

Require specific value#

Use const to require an exact enum value:

enum Status {
  STATUS_UNSPECIFIED = 0;
  STATUS_PENDING = 1;
  STATUS_ACTIVE = 2;
  STATUS_ARCHIVED = 3;
}

message Config {
  // Must be STATUS_ACTIVE
  Status status = 1 [(buf.validate.field).enum.const = 2];
}

Try it in the Playground ↗

Example values#

Use example to document valid enum values. These don't affect validation but help consumers understand what values are expected:

enum Priority {
  PRIORITY_UNSPECIFIED = 0;
  PRIORITY_LOW = 1;
  PRIORITY_MEDIUM = 2;
  PRIORITY_HIGH = 3;
  PRIORITY_CRITICAL = 4;
}

message Request {
  // Only allow low, medium, or high priority and provide an example.
  Priority priority = 1 [
    (buf.validate.field).enum = { in: [1, 2, 3] },
    (buf.validate.field).enum.example = 1,  // PRIORITY_LOW
    (buf.validate.field).enum.example = 2   // PRIORITY_MEDIUM
  ];
}

Reference#

Oneofs#

Protovalidate can validate traditional Protobuf oneof fields, but consider this: Protobuf's oneof generates awful code in some languages (such as Go) and has frustrating limitations, like the inability to use repeated and map fields and backwards-compatibility headaches.

To fix this, Protovalidate introduces a message-level oneof rule that gives you the functionality you want, without the headaches.

When to use which

Use Protovalidate's message (buf.validate.message).oneof rule when:

  • You want cleaner generated code (just regular fields, no wrapper types)
  • You need repeated or map fields
  • You need better schema evolution (no backwards-compatibility restrictions)

Use a Protobuf oneof when:

  • You need to support existing oneof fields

Message oneof rule#

Enforcing "one of" semantics#

Use (buf.validate.message).oneof's fields to state that only one of the fields listed can be present:

message SearchQuery {
  option (buf.validate.message).oneof = {
    // At most one search method can be used
    fields: ["keyword", "tags", "category"],
  };

  string keyword = 1;
  // Protobuf oneof doesn't allow repeated fields, but Protovalidate's
  // oneof rule supports them!
  repeated string tags = 2;
  string category = 3;
}

Try it in the Playground ↗

Requiring a oneof value#

Use required to state that exactly one of the fields listed must be present.

message SearchQuery {
  option (buf.validate.message).oneof = {
    // At most one search method can be used
    fields: ["keyword", "tags", "category"],
    // One search method must be present
    required: true
  };

  string keyword = 1;
  // Protobuf oneof doesn't allow repeated fields, but Protovalidate's
  // oneof rule supports them!
  repeated string tags = 2;
  string category = 3;
}

Try it in the Playground ↗

Note that adding a field to a oneof also sets IGNORE_IF_ZERO_VALUE on the fields.

Validating fields in a oneof#

Protovalidate honors rules on the fields within the oneof exactly as you'd expect: fields that are present are validated, and rules for the others are ignored.

In this example:

  • If a message only provides tags, any rules on keyword or category are ignored.
  • If a message attempts to set more than one field in the oneof, rules for each of the values are applied. If keyword was set to "foo" and category was set to "bar", validation errors would include errors for both fields and an error stating that only one field in the oneof should be set.
message SearchQuery {
  option (buf.validate.message).oneof = {
     // At most one search method can be used
    fields: ["keyword", "tags", "category"],
    // At least one must be provided
    required: true
  };

  // Field rules will only be applied when this field is present
  string keyword = 1 [
    (buf.validate.field).string.min_len = 5
  ];
  repeated string tags = 2 [
   (buf.validate.field).repeated.min_items = 1
  ];
  // Field rules will only be applied when this field is present
  string category = 3 [
    (buf.validate.field).string.min_len = 5
  ];
}

Try it in the Playground ↗

Note that adding a field to a oneof also sets IGNORE_IF_ZERO_VALUE on the fields.

Protobuf oneof#

Use Protobuf's oneof construct to require exactly one field in the oneof. Without required, the oneof is optional—zero or one field can be set. Note that you can apply rules to the fields within the oneof.

message UserRef {
  oneof identifier {
    // Exactly one of id or username must be set
    option (buf.validate.oneof).required = true;

    string id = 1 [(buf.validate.field).string.uuid = true];
    string username = 2 [(buf.validate.field).string.min_len = 3];
  }
}

Try it in the Playground ↗

Reference#

Nested messages#

Nested messages are validated automatically. Use ignore to skip validation on messages.

In this example, if an Order's items contains any LineItem with an empty product_id or zero quantity, validation fails.

message LineItem {
  string product_id = 1 [
    (buf.validate.field).string.min_len = 1
  ];
  uint32 quantity = 2 [
    (buf.validate.field).uint32.gt = 0
  ];
}

message Order {
  repeated LineItem items = 1 [
    (buf.validate.field).repeated.min_items = 0
  ];
}

Try it in the Playground ↗

Ignoring#

Use ignore = IGNORE_ALWAYS to ignore rules on a nested message:

message InnerMessage {
  string value = 1 [
    (buf.validate.field).string.min_len = 1
  ];
}

message OuterMessage {
  InnerMessage inner = 1 [
    (buf.validate.field).ignore = IGNORE_ALWAYS
  ];
}

Try it in the Playground ↗

Reference#

Required fields#

Use required to state that a field must be set in a message. Don't use it to check the value of a field: empty strings can be valid in proto2, but not proto3!

  • Use required when you have considered field presence, understand implicit presence, and need to describe if a field must be set.
  • Don't use required for semantic validation, like requiring a non-empty string. Instead, use rules like min_len for strings or gte for numbers.

This can be confusing, but it has a simple cause: sometimes it's impossible to distinguish a default value from a field that wasn't set. Unless you need to work with field presence, it's safer to skip required and use simpler rules instead.

Field presence#

All Protobuf fields have either explicit or implicit presence:

  • Explicit presence fields - The API stores both the field value and whether the field has been set. This allows the runtime to distinguish between "not set", "set to default value", and "cleared".
  • Implicit presence fields - The API stores only the field value. When a field has its default value, the runtime can't distinguish between three possible states: (1) never set, (2) explicitly set to the default value, or (3) cleared by setting to the default value.

This explains why a proto2 message with Protovalidate's required rule on a string field is valid even if the string is empty: all scalar fields in proto2 have explicit presence. When Protovalidate enforces the required rule, it can check whether the field was set (even if set to an empty string), and the rule passes. See an example ↗

In proto3, the same validation rule fails for an empty string. Because proto3 scalar fields (without optional) have implicit presence, and the default value for a string is an empty string, Protovalidate can't determine if the field was set or simply has its default value. The field must be set for the required rule to pass, so when presence can't be determined, validation fails. See an example ↗

Therefore, it's important to understand the rules for when fields track presence. The rules are slightly different for proto2 and proto3:

Field Type proto3 proto2 Example
Scalar without optional ✗ No tracking N/A (not allowed) string name = 1
Scalar with optional ✓ Tracks ✓ Tracks optional string name = 1
Nested message ✓ Tracks ✓ Tracks Address addr = 1
Part of a oneof ✓ Tracks ✓ Tracks Fields inside oneof blocks
repeated field ✗ No tracking ✗ No tracking repeated string tags = 1
map field ✗ No tracking ✗ No tracking map<string, string> attrs = 1

If you're unsure about presence for your specific case, see the Field Presence guide for details.

Required cheat sheet#

Fields with presence#

For fields that track presence:

  • If no value is set, Protovalidate skips validation for the field.
  • required means the field must be set (present) in the message.
  • required doesn't cause a failure if the field is set to a default value (empty string for string, 0 for numbers, etc.).

proto3 example ↗ | proto2 example ↗

Fields without presence#

For fields that don't track presence:

  • Protovalidate always validates the field, but if required fails, other rules are skipped.
  • required means the field can't be the default value.
  • required causes a failure if the field is set to a default value (empty string for string, 0 for numbers, etc.).

The required rule is useful for implicitly present fields when:

  • You want to explicitly document "default values not allowed" without other constraints
  • No other rule rejects the default value for your use case

proto3 example ↗ | proto2 example ↗

Reference#

Ignore rules#

Use ignore to skip validation rules for a field, including nested messages.

Skip validation entirely#

Use ignore = IGNORE_ALWAYS to completely disable validation for a field, no matter its value:

message Config {
  // The min_len rule is never enforced.
  optional string internal_debug_data = 1 [
    (buf.validate.field).string.min_len = 10,
    (buf.validate.field).ignore = IGNORE_ALWAYS
  ];
}

IGNORE_ALWAYS and required#

IGNORE_ALWAYS supersedes required. Using it on a field marked required ignores required:

syntax = "proto3";

package playground;

import "buf/validate/validate.proto";

message IgnoreAndRequired {
  // IGNORE_ALWAYS supersedes the `required` rule, including implicitly
  // present default values.
  //
  // A message sent with an empty string for "foo" will not fail.
  string foo = 1 [
    (buf.validate.field).ignore = IGNORE_ALWAYS,
    (buf.validate.field).required = true
  ];

  // IGNORE_ALWAYS supersedes the `required` rule.
  //
  // A message sent without a value for "bar" will not fail.
  string bar = 2 [
    (buf.validate.field).ignore = IGNORE_ALWAYS,
    (buf.validate.field).required = true
  ];
}
Try it in the Playground ↗

syntax = "proto2";

package playground;

import "buf/validate/validate.proto";

message User {
  // IGNORE_ALWAYS supersedes the `required` rule, including explicit
  // presence fields.
  //
  // A message that doesn't set the "name" field will not fail.
  optional string name = 1 [
    (buf.validate.field).ignore = IGNORE_ALWAYS,
    (buf.validate.field).required = true
  ];
}
Try it in the Playground ↗

Skip nested message validation#

Use ignore on message fields to skip validating the nested message:

message User {
  // Validate the user's name
  string name = 1 [(buf.validate.field).string.min_len = 1];

  // Require a primary address and enforce its rules
  Address primary_address = 2 [
    (buf.validate.field).required = true
  ] ;

  // Skip any rules for the secondary address
  Address secondary_address = 3 [
    (buf.validate.field).ignore = IGNORE_ALWAYS
  ];
}

message Address {
  string street = 1 [(buf.validate.field).string.min_len = 1];
  string city = 2 [(buf.validate.field).string.min_len = 1];
}

Ignore default (zero) values#

Use ignore = IGNORE_IF_ZERO_VALUE to skip validation when an implicitly present field's value is its default value.

This is only useful for fields with implicit presence: Protovalidate always skips validation for unset fields with presence tracking (fields with explicit presence). Using IGNORE_IF_ZERO_VALUE on a field that tracks presence is the same as the default behavior and causes a buf lint error.

Examples:

syntax = "proto3";

message Product {
  // Without ignore: an implicitly-present empty string fails validation
  string name = 1 [
    (buf.validate.field).string.min_len = 1
  ];

  // With ignore: validation for an implicitly-present empty string
  // is skipped entirely
  string description = 2 [
    (buf.validate.field).string.min_len = 10,
    (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE
  ];
}
syntax = "proto2";

message Product {
  // Without ignore: an implicitly-present empty list fails validation
  repeated tags = 1 [
    (buf.validate.field).repeated.min_items = 1
  ];

  // With ignore: validation for implicitly-present empty map
  // is skipped entirely
  map <string, string>custom_attributes = 2 [
    (buf.validate.field).string.min_pairs = 1,
    (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE
  ];
}

Field presence and IGNORE_IF_ZERO_VALUE#

Understanding IGNORE_IF_ZERO_VALUE requires understanding field presence. All Protobuf fields have either explicit or implicit presence:

  • Explicit presence fields - The API stores both the field value and whether the field has been set. This allows the runtime to distinguish between "not set", "set to default value", and "cleared".
  • Implicit presence fields - The API stores only the field value. When a field has its default value, the runtime can't distinguish between three possible states: (1) never set, (2) explicitly set to the default value, or (3) cleared by setting to the default value.

This is admittedly confusing without an example. Across all versions of Protobuf, repeated fields don't track presence, and their default value is an empty list. If a message is sent without a value for a repeated field, its value at runtime is the default: an empty list. There's no way to tell between default values and fields explicitly set to the default value.

When Protovalidate encounters a field with implicit presence, it can't distinguish between "not set" and "set to default value," so it treats all default values as "set" and enforces any rules on the field:

syntax = "proto3";

message Example {
  // Has presence (optional keyword) - only validated when set
  optional string nickname = 1 [
    (buf.validate.field).string.min_len = 3
  ];

  // No presence - always validated, even if unset
  string username = 2 [
    (buf.validate.field).string.min_len = 3
  ];
}
syntax = "proto2";

message Example {
  // Has presence (proto2 scalars always do) - only validated when set
  optional string nickname = 1 [
    (buf.validate.field).string.min_len = 3
  ];

  // No presence (repeated fields never do) - always validated
  repeated string tags = 2 [
    (buf.validate.field).repeated.min_items = 1
  ];
}

When using IGNORE_IF_ZERO_VALUE, it's important to understand the rules for when fields track presence. The rules are slightly different for proto2 and proto3:

Field Type proto3 proto2 Example
Scalar without optional ✗ No tracking N/A (not allowed) string name = 1
Scalar with optional ✓ Tracks ✓ Tracks optional string name = 1
Nested message ✓ Tracks ✓ Tracks Address addr = 1
Part of a oneof ✓ Tracks ✓ Tracks Fields inside oneof blocks
repeated field ✗ No tracking ✗ No tracking repeated string tags = 1
map field ✗ No tracking ✗ No tracking map<string, string> attrs = 1

If you're unsure about presence for your specific case, see the Field Presence guide for details.

IGNORE_IF_ZERO_VALUE and required#

required takes precedence over IGNORE_IF_ZERO_VALUE, behaving as if ignore wasn't set.

syntax = "proto3";

package playground;

import "buf/validate/validate.proto";

message IgnoreZeroValueAndRequired {
  // `required` takes precedence over IGNORE_IF_ZERO VALUE for unset fields
  // (default values).
  //
  // This will fail the `required` if "foo" isn't provided, but not
  // the `min_len` rule.
  string foo = 1 [
    (buf.validate.field).required = true,
    (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
    (buf.validate.field).string.min_len = 10
  ];

  // `required` takes precedence over IGNORE_IF_ZERO VALUE for implictly-present
  //  fields set to their default values.
  //
  // This will fail the `required` if "bar" is set to an empty string, but not
  // the `min_len` rule.
  string bar = 3 [
    (buf.validate.field).required = true,
    (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
    (buf.validate.field).string.min_len = 10
  ];
}

Try it in the Playground ↗

Reference#

Field relationships and domain logic#

The standard rules documented on this page validate single fields. When you need to validate relationships between fields or enforce complex domain logic, use CEL (Common Expression Language) to write custom rules.

When you need custom rules#

Use message-level CEL when validation involves multiple fields:

  • Comparing fields (start_date < end_date)
  • Conditional requirements (if status == SHIPPED, then tracking_number is required)
  • Aggregate constraints (all product_id in repeated line_item messages must be unique)
  • Any other complex business rules that can't be expressed with standard rules

Use field-level CEL for complex single-field validation like:

  • Domain-specific formats
  • Complex mathematical constraints
  • Validation that requires multiple conditions not handled by standard rules

See Custom rules to learn how handle these validation problems with CEL.

Reference#