Validating with protovalidate

Copy Markdown View Source

PB.Validate runs protovalidate rules at runtime. Rules are read from the same schema you encode against and applied through the same projections and adapters, so the value being validated is exactly the value you hand to PB.encode/4.

Annotate rules in proto source

protovalidate rules are declared with buf.validate options in your .proto files:

import "buf/validate/validate.proto";

message Person {
  string name  = 1 [(buf.validate.field).string.min_len = 1];
  int32  age   = 2 [(buf.validate.field).int32.gte = 0];
  string email = 3 [(buf.validate.field).string.email = true];
}

Generate the descriptor set with --include_imports, as for any schema (buf build includes the imports by default). The rules are read through the set's own buf/validate/validate.proto, so PB.compile/2 rejects a set without it, as it does any set that lacks an imported file. A set compiled anyway, with allow_missing_imports: true, can't have its rules read, so PB.Validate raises a PB.SchemaError for it rather than compile no rules.

An edition 2024 file may import buf/validate/validate.proto with import option instead, but buf build never includes option imports, and PB.compile/2 allows them to be missing, so PB.Validate raises for such a set too. Import it with a regular import, or build the set with protoc --include_imports.

Compile the rules

PB.Validate is a schema plugin (PB.Schema.Plugin): list it in :plugins when you compile the schema, and it compiles the rules into the schema once, type-checking every CEL expression:

schema = PB.compile(descriptor_set, plugins: [PB.Validate])

or, for a schema module:

defmodule MyApp.Schema do
  use PB.Schema, descriptor: "priv/proto/schema.binpb", plugins: [PB.Validate]
end

Rules that don't fit the schema, such as CEL that doesn't type-check or string rules on an integer field, raise PB.Validate.CompileError there. A schema compiled without the plugin can still encode and decode, but PB.Validate rejects it with an ArgumentError.

Validate a message

case PB.Validate.validate(person, schema, :"mypackage.Person") do
  :ok ->
    :ok

  {:error, %PB.Validate.Error{violations: violations}} ->
    # each is a %PB.Validate.Violation{}
    {:error, violations}
end

validate/3 returns :ok or {:error, %PB.Validate.Error{}}. A bang variant validate!/3 raises on violations. Schema modules compiled with the plugin expose validate/2,3 and validate!/2,3 directly:

:ok = MyApp.Schema.validate(person, :"mypackage.Person")

The input is the same public term shape accepted by PB.encode/4: canonical maps, represented structs, identity oneofs, unwrapped messages, and adapted values are each projected one message layer at a time before field, message, and CEL rules read protobuf fields.

Options

PB.Validate.validate/4 and the schema module functions accept:

OptionValuesNotes
:contextkeyword | map | %PB.Validate.Context{}Runtime context for CEL evaluation.
:nowinteger | DateTime.t() | {:timestamp, unix_nanoseconds}Value bound to the now variable in CEL rules when :context is not supplied.

Unknown options are rejected with a PB.OptionError.

Errors vs violations

A violation (a rule that the message failed) is data: it comes back in {:error, %PB.Validate.Error{}}. A caller-contract or evaluation problem is raised instead — an unknown validation message, a message-name metadata mismatch, non-map input, or a projection failure raises a PB.Error (PB.SchemaError/PB.ValueError) with operation: :validate, and a CEL constraint that fails at runtime raises CEL.Error. Match on %PB.Validate.Violation{} for rule results; let contract errors surface.

Conformance

PB.Validate passes the full upstream protovalidate conformance suite at its pinned commit. See Conformance status.

CEL expression rules

protovalidate's cel rules are evaluated with cel_ex's CEL against proto-shaped values. Registering your own functions callable from those expressions is supported, but that API is still being reshaped and is not part of the published interface yet; it is planned for a later release.