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];
}
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];
}
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];
}
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"]
}];
}
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"];
}
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"];
}
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}$"];
}
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.
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];
}
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];
}
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];
}
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];
}
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:
- String rules
- Bytes rules
- Numeric rules: Float, Double, Int32, Int64, UInt32, UInt64, SInt32, SInt64, Fixed32, Fixed64, SFixed32, SFixed64
- Bool 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}
}];
}
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}];
}
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}
]
}];
}
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}
}];
}
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];
}
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}
}];
}
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}];
}
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"
]
}];
}
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];
}
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
}];
}
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];
}
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
];
}
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)"
};
}
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];
}
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]
}];
}
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];
}
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
oneoffields
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;
}
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;
}
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 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.
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#
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];
}
}
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
];
}
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
];
}
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
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#
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.
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#
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#
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
];
}
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#
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 = "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
];
}
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.