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
Section titled “Fundamentals”Protovalidate follows these core principles:
Rules are composable
Section titled “Rules are composable”You can combine multiple rules on a single field. All rules must pass for validation to succeed:
import "buf/validate/validate.proto";
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
Section titled “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.
import "buf/validate/validate.proto";
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
Section titled “Implicit presence always validates”Fields without explicit presence (proto3 scalars without optional, repeated, map) are always validated, even when they have their default value:
import "buf/validate/validate.proto";
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
Section titled “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:
import "buf/validate/validate.proto";
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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.
import "buf/validate/validate.proto";
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];}Exact length
Section titled “Exact length”Use len to require an exact length.
For strings, len counts characters (Unicode code points). Use len_bytes to count bytes instead.
import "buf/validate/validate.proto";
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];}Numeric ranges
Section titled “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.
import "buf/validate/validate.proto";
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];}Sets of values
Section titled “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.
import "buf/validate/validate.proto";
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"] }];}Constant/exact values
Section titled “Constant/exact values”Use const to require a field to match an exact value. Available for all scalar types:
import "buf/validate/validate.proto";
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"];}Prefix, suffix, and contains
Section titled “Prefix, suffix, and contains”Use prefix, suffix, and contains to validate strings and byte sequences. not_contains is also available for strings.
import "buf/validate/validate.proto";
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"];}Regular expressions
Section titled “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.
import "buf/validate/validate.proto";
message Order { // Order IDs are always ORD- followed by 8 digits string order_id = 1 [(buf.validate.field).string.pattern = "^ORD-[0-9]{8}$"];}Email addresses
Section titled “Email addresses”The email rule ensures the string is a valid email address format according to the HTML standard.
This intentionally deviates from RFC 5322, which allows many unexpected forms of email addresses and easily matches typographical errors.
import "buf/validate/validate.proto";
message User { string email = 1 [(buf.validate.field).string.email = true];}UUIDs and TUUIDs
Section titled “UUIDs and TUUIDs”Use uuid to validate standard UUID format, or tuuid for UUIDs without dashes:
import "buf/validate/validate.proto";
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];}Hostnames and URIs
Section titled “Hostnames and URIs”The hostname and uri rules validate hostnames and URIs, respectively. Use uri_ref to also allow relative references:
import "buf/validate/validate.proto";
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];}Network validation
Section titled “Network validation”Protovalidate includes validation rules for IP addresses, network prefixes, and related formats:
import "buf/validate/validate.proto";
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];}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
Section titled “HTTP headers”Use well_known_regex for HTTP header validation:
import "buf/validate/validate.proto";
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];}The default expressions follow RFC 7230. Set strict = false to allow any values except \r\n\0:
import "buf/validate/validate.proto";
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
Section titled “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:
import "buf/validate/validate.proto";
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
Section titled “Reference”For the complete list of scalar field rules:
- String rules
- Bytes rules
- Numeric rules: Float, Double, Int32, Int64, UInt32, UInt64, SInt32, SInt64, Fixed32, Fixed64, SFixed32, SFixed64
- Bool rules
Durations
Section titled “Durations”Duration rules apply to google.protobuf.Duration fields. Use duration rules to validate time spans like timeouts, delays, or intervals.
Duration ranges
Section titled “Duration ranges”Use gt, gte, lt, and lte to validate durations:
import "buf/validate/validate.proto";import "google/protobuf/duration.proto";
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} }];}Exact duration
Section titled “Exact duration”Use const to require an exact duration:
import "buf/validate/validate.proto";import "google/protobuf/duration.proto";
message Schedule { // Interval must be exactly 5 minutes google.protobuf.Duration interval = 1 [(buf.validate.field).duration.const = {seconds: 300}];}Sets of durations
Section titled “Sets of durations”Use in to restrict values to specific durations, or not_in to exclude values:
import "buf/validate/validate.proto";import "google/protobuf/duration.proto";
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} ] }];}Reference
Section titled “Reference”Timestamps
Section titled “Timestamps”Timestamp rules apply to google.protobuf.Timestamp fields. Use timestamp rules to validate dates, times, and temporal constraints.
Timestamp ranges
Section titled “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.
import "buf/validate/validate.proto";import "google/protobuf/timestamp.proto";
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} }];}Relative to current time
Section titled “Relative to current time”Use lt_now and gt_now to validate timestamps relative to the current time:
import "buf/validate/validate.proto";import "google/protobuf/timestamp.proto";
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];}Time windows
Section titled “Time windows”Use within with lt_now or gt_now to validate timestamps within a duration of the current time:
import "buf/validate/validate.proto";import "google/protobuf/timestamp.proto";
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} }];}Exact timestamp
Section titled “Exact timestamp”Use const to require an exact timestamp.
Use seconds and nanos of time since Unix epoch.
import "buf/validate/validate.proto";import "google/protobuf/timestamp.proto";
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}];}Reference
Section titled “Reference”Any fields
Section titled “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
Section titled “Restrict type URLs”Use in to allow only specific message types, or not_in to exclude types:
import "buf/validate/validate.proto";import "google/protobuf/any.proto";
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" ] }];}Reference
Section titled “Reference”Repeated and map fields
Section titled “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.
Use min_items and max_items for repeated fields, or min_pairs and max_pairs for maps:
import "buf/validate/validate.proto";
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];}Validate items
Section titled “Validate items”Use items to apply validation rules to each element in a repeated field:
import "buf/validate/validate.proto";
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 }];}Unique items
Section titled “Unique items”Use unique to ensure all items in a repeated field are unique. Works for scalar and enum types only:
import "buf/validate/validate.proto";
message Permissions { // No duplicate user IDs repeated string user_ids = 1 [(buf.validate.field).repeated.unique = true];}Validate keys and values
Section titled “Validate keys and values”Use keys and values to apply validation rules to map entries:
import "buf/validate/validate.proto";
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 ];}Nested messages
Section titled “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:
import "buf/validate/validate.proto";
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)" };}Learn more about CEL in custom rules.
Reference
Section titled “Reference”Enum validation ensures fields contain valid enum values and can restrict which values are allowed.
Reject undefined values
Section titled “Reject undefined values”Use defined_only to reject any value not defined in the enum:
import "buf/validate/validate.proto";
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];}Restrict to specific values
Section titled “Restrict to specific values”Use in to allow only specific enum values, or not_in to exclude values:
import "buf/validate/validate.proto";
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] }];}Require specific value
Section titled “Require specific value”Use const to require an exact enum value:
import "buf/validate/validate.proto";
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];}Example values
Section titled “Example values”Use example to document valid enum values. These don’t affect validation but help consumers understand what values are expected:
import "buf/validate/validate.proto";
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
Section titled “Reference”Oneofs
Section titled “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
oneoffields
Message oneof rule
Section titled “Message oneof rule”Enforcing “one of” semantics
Section titled “Enforcing “one of” semantics”Use (buf.validate.message).oneof’s fields to state that only one of the fields listed can be present:
import "buf/validate/validate.proto";
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;}Requiring a oneof value
Section titled “Requiring a oneof value”Use required to state that exactly one of the fields listed must be present.
import "buf/validate/validate.proto";
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;}Note that adding a field to a oneof also sets IGNORE_IF_ZERO_VALUE on the fields.
Validating fields in a oneof
Section titled “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 onkeywordorcategoryare ignored. - If a message attempts to set more than one field in the
oneof, rules for each of the values are applied. Ifkeywordwas set to “foo” andcategorywas set to “bar”, validation errors would include errors for both fields and an error stating that only one field in theoneofshould be set.
import "buf/validate/validate.proto";
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 ];}Note that adding a field to a oneof also sets IGNORE_IF_ZERO_VALUE on the fields.
Protobuf oneof
Section titled “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.
import "buf/validate/validate.proto";
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]; }}Reference
Section titled “Reference”Nested messages
Section titled “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.
import "buf/validate/validate.proto";
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 ];}Ignoring
Section titled “Ignoring”Use ignore = IGNORE_ALWAYS to ignore rules on a nested message:
import "buf/validate/validate.proto";
message InnerMessage { string value = 1 [ (buf.validate.field).string.min_len = 1 ];}
message OuterMessage { InnerMessage inner = 1 [ (buf.validate.field).ignore = IGNORE_ALWAYS ];}Reference
Section titled “Reference”Required fields
Section titled “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
requiredwhen you have considered field presence, understand implicit presence, and need to describe if a field must be set. - Don’t use
requiredfor semantic validation, like requiring a non-empty string. Instead, use rules likemin_lenfor strings orgtefor 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
Section titled “Field presence”All Protobuf fields have either explicit or implicit presence:
- Explicit presence fields - The message stores both the field value and whether the field has been set. This allows the runtime to distinguish between “not set” and “set”, even when the field is set to its default value.
- Implicit presence fields - The message stores only the field value. When a field has its default value, the runtime can’t distinguish between two possible states: (1) never set, or (2) explicitly set 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.
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.
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
Section titled “Required cheat sheet”Fields with presence
Section titled “Fields with presence”For fields that track presence:
- If no value is set, Protovalidate skips validation for the field.
requiredmeans the field must be set (present) in the message.requireddoesn’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
Section titled “Fields without presence”For fields that don’t track presence:
- Protovalidate always validates the field, but if
requiredfails, other rules are skipped. requiredmeans the field can’t be the default value.requiredcauses 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
Section titled “Reference”Ignore rules
Section titled “Ignore rules”Use ignore to skip validation rules for a field, including nested messages.
Skip validation entirely
Section titled “Skip validation entirely”Use ignore = IGNORE_ALWAYS to completely disable validation for a field, no matter its value:
import "buf/validate/validate.proto";
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
Section titled “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 ];}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 ];}Skip nested message validation
Section titled “Skip nested message validation”Use ignore on message fields to skip validating the nested message:
import "buf/validate/validate.proto";
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
Section titled “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";
import "buf/validate/validate.proto";
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";
import "buf/validate/validate.proto";
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
Section titled “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 message stores both the field value and whether the field has been set. This allows the runtime to distinguish between “not set” and “set”, even when the field is set to its default value.
- Implicit presence fields - The message stores only the field value. When a field has its default value, the runtime can’t distinguish between two possible states: (1) never set, or (2) explicitly set 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";
import "buf/validate/validate.proto";
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";
import "buf/validate/validate.proto";
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
Section titled “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 implicitly-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 ];}Reference
Section titled “Reference”Field relationships and domain logic
Section titled “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
Section titled “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.