Skip to content

Guide

Validation and errors

Requests are checked against protovalidate rules written in the .proto file, before a handler sees them.

import "buf/validate/validate.proto";

message Book {
  string title = 2 [(buf.validate.field).string = {min_len: 1, max_len: 200}];
  int64 pages = 3 [(buf.validate.field).int64 = {gte: 0, lte: 10000}];
  repeated string tags = 5 [(buf.validate.field).repeated = {
    max_items: 5
    items: {string: {min_len: 1}}
  }];
  string contact = 6 [
    (buf.validate.field).string.email = true,
    (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE
  ];
}

message CreateBookRequest {
  string shelf = 1;
  Book book = 2 [(buf.validate.field).required = true];
}

Nothing is switched on. The validator for each method is built from the request type's descriptor when the endpoint is made. It runs on every protocol and on each message of a client or bidi stream.

A violation

A request that breaks a rule fails with INVALID_ARGUMENT and a google.rpc.BadRequest with one entry per broken rule. Each entry names the field by its path, such as book.tags[1], gives the rule's id as its reason and protovalidate's description of what was broken. A rule written on a message names the field the message sits in, or none at the top level.

Every protocol carries the same status in its own form:

Protocol Status Details
gRPC grpc-status: 3 grpc-status-details-bin, a binary google.rpc.Status
gRPC-Web grpc-status: 3 the same, in the trailer frame
Connect 400, invalid_argument details in the error JSON, base64
REST 400 details in the google.rpc.Status JSON, with an @type
{
  "code": 3,
  "message": "invalid request: book.title: must be at least 1 characters",
  "details": [
    {
      "@type": "type.googleapis.com/google.rpc.BadRequest",
      "fieldViolations": [
        {
          "field": "book.title",
          "description": "must be at least 1 characters",
          "reason": "string.min_len"
        }
      ]
    }
  ]
}

Any RpcError a handler throws travels the same way, with its details. Over REST, details are rendered by their JSON mapping. That covers every google.rpc detail (BadRequest, ErrorInfo, RetryInfo, QuotaFailure, PreconditionFailure, ResourceInfo, RequestInfo, Help, LocalizedMessage, DebugInfo) and every message the server's services reach. A detail of any other type is left out of the REST reply; the RPC protocols carry it as bytes.

What is checked

Every protovalidate rule, with protovalidate's semantics.

Kind Rules
Every field required, ignore, cel, cel_expression
Messages cel, cel_expression, oneof
Numbers const, lt, lte, gt, gte, in, not_in, and finite for floats
Strings const, len, min_len, max_len, their _bytes forms, pattern, prefix, suffix, contains, not_contains, in, not_in
String formats email, hostname, ip, ipv4, ipv6, uri, uri_ref, address, uuid, tuuid, ulid, ip_with_prefixlen, ipv4_with_prefixlen, ipv6_with_prefixlen, ip_prefix, ipv4_prefix, ipv6_prefix, host_and_port, protobuf_fqn, protobuf_dot_fqn, well_known_regex
Bytes const, len, min_len, max_len, pattern, prefix, suffix, contains, in, not_in, ip, ipv4, ipv6, uuid
Booleans and enums const, and defined_only, in, not_in for enums
Repeated fields min_items, max_items, unique, and items for each element
Maps min_pairs, max_pairs, keys, values
Timestamps const, lt, lte, gt, gte, lt_now, gt_now, within
Durations const, lt, lte, gt, gte, in, not_in
Any and field masks in, not_in, and const for field masks
Oneofs required
Predefined rules rules of your own, declared with (buf.validate.predefined).cel on an extension of a rule message

A proto3 field without optional is checked even when it holds its zero value. Add ignore = IGNORE_IF_ZERO_VALUE to leave an empty field alone.

pattern and matches() use RE2. CEL expressions are evaluated with CEL's standard library, its string extensions and protovalidate's functions (isEmail, isHostname, isIp, isIpPrefix, isUri, isUriRef, isHostAndPort, isNan, isInf, unique, getField). lt_now, gt_now, within and now in an expression compare against the server's clock when the message is checked.

message Book {
  option (buf.validate.message).cel = {
    id: "book.tags_within_pages"
    message: "a book cannot carry more tags than it has pages"
    expression: "this.pages == 0 || size(this.tags) <= this.pages"
  };

  int64 pages = 3;
  repeated string tags = 5;
  string isbn = 7 [(buf.validate.field).string.pattern = "^[0-9]{13}$"];
  google.protobuf.Timestamp due = 8 [(buf.validate.field).timestamp.gt_now = true];
}

protovalidate's conformance suite runs against the validator in nix flake check, every case passing.