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]
endRules 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}
endvalidate/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:
| Option | Values | Notes |
|---|---|---|
:context | keyword | map | %PB.Validate.Context{} | Runtime context for CEL evaluation. |
:now | integer | 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.