Skip to content

Generated code already checks your types. A string field always holds a string, and an int32 always holds a number. protovalidate goes further. You write range and format rules straight into the schema, next to the fields they constrain.

Generated Protobuf code already enforces the schema's structural types. A string field holds a string. An integer field holds a number. A repeated field is a collection, and a nested message has the shape the schema defines.

This is useful, but it is intentionally limited. A schema type can tell you a field is a string. It cannot tell you the string is a valid email address. It cannot tell you an age is in range, or that two fields satisfy some business rule between them.

The next layer describes those expectations in the schema itself. You add annotations, and every tool across every service and language reads the same rules back out.

message SignupRequest {
  string email = 1;
  uint32 age = 2;
  repeated string roles = 3;
}

A protovalidate rule is a field option. You write it in the brackets after the field, right where any other option goes. That means the rule lives in the same file as the field it constrains, so it goes through review with the field and ends up in the same generated descriptors. Every language's protovalidate library reads the rules back out of those descriptors, and enforces them at runtime.

A file that uses rules imports them like any other schema, with import "buf/validate/validate.proto";. Nothing else about the field changes. Drop the options and you have plain Protobuf again.

string email = 3 [(buf.validate.field).string.email = true];
(buf.validate.field)
The option protovalidate defines. The parentheses mark it as an extension, so this is a custom option rather than one built into Protobuf.
.string.email
The rule: the field's type, then a rule that type offers. A uint32 field takes numeric rules, a repeated field takes list rules.
= true
The value. Some rules are toggles; others take a number, a string, or a list of allowed values.

Length and format

ACCOUNT.PROTO
message Account {
  string id = 1 [
    (buf.validate.field).string.uuid = true
  ];

  string name = 2 [
    (buf.validate.field).string.min_len = 2,
    (buf.validate.field).string.max_len = 50
  ];

  string email = 3 [
    (buf.validate.field).string.email = true
  ];
}

Ranges

MEMBERSHIP.PROTO
message Membership {
  uint32 age = 1 [
    (buf.validate.field).uint32 = {
      gte: 18
      lt: 120
    }
  ];
}

Enum values

ASSIGNMENT.PROTO
message Assignment {
  Role role = 1 [
    (buf.validate.field).enum = {
      defined_only: true
    }
  ];

  enum Role {
    ROLE_UNSPECIFIED = 0;
    ROLE_USER = 1;
    ROLE_ADMIN = 2;
  }
}

Must be set

LOGIN_EVENT.PROTO
message LoginEvent {
  string user_id = 1 [
    (buf.validate.field).string.uuid = true
  ];

  google.protobuf.Timestamp event_time = 2 [
    (buf.validate.field).required = true
  ];
}

Lists and maps

TEAM.PROTO
message Team {
  repeated string roles = 1 [
    (buf.validate.field).repeated = {
      min_items: 1
      unique: true
      items: {
        string: {min_len: 1}
      }
    }
  ];

  map<string, int32> quotas = 2 [
    (buf.validate.field).map = {
      max_pairs: 10
      values: {
        int32: {gte: 0}
      }
    }
  ];
}

Rules that span fields

DATE_RANGE.PROTO
message DateRange {
  google.protobuf.Timestamp start = 1 [
    (buf.validate.field).required = true
  ];
  google.protobuf.Timestamp end = 2 [
    (buf.validate.field).required = true
  ];

  option (buf.validate.message).cel = {
    id: "date_range.start_before_end"
    message: "start must be before end"
    expression: "this.start < this.end"
  };
}

The lab below runs rules like these in your browser. Edit the data, or open the schema and change the rules, and the violations update right away.

The lab enforces rules like those above, written in CEL and read straight out of the schema by protovalidate . There is no generated validation code and no rules living off in a service. Edit the JSON, load an example, or edit the schema itself and watch the violations on the right keep up.

Test Data (JSON)

JSON input
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Hiro Protagonist",
  "email": "hiro@metaverse.com",
  "age": 30,
  "role": 2,
  "birthDate": {
    "year": 1996,
    "month": 1,
    "day": 1
  }
}

Rules Enforcement

Validation status

Waiting for
valid input

Validation Strategy

By putting validation in the schema, you ensure that every part of your system enforcing the contract applies the exact same rules. This eliminates "validation drift" across your entire stack, not only between microservices. For instance, you can use the same rules to validate a form on your web frontend (using TypeScript) before the request ever hits your backend (running Go, Java, etc.).

Next

Efficiency

How much space the binary encoding actually saves against JSON, and what happens to that gap once you gzip both.