prost-protovalidate-build
Compile-time code generator for Protocol Buffer validation using buf.validate rules, built for prost and buffa — direct field access at runtime, no reflection, no CEL interpreter on the hot path.
Generates impl prost_protovalidate::Validate for messages with standard buf.validate field constraints. Messages containing CEL expressions are, per your choice, skipped with a cargo:warning (validate them via the runtime prost_protovalidate::Validator), turned into a hard build error (fail_on_runtime_only), or — on the buffa backend — routed through an embedded runtime engine (runtime_bridge) so the whole schema, CEL included, validates through one uniform msg.validate().
Usage
Add the dependency to your Cargo.toml:
[]
= "0.14"
= "0.6"
In your build.rs:
Include the generated validation code alongside prost output:
include!;
Backends
Generated code targets prost message types by default. When the message
types are generated by buffa-build (or a wrapper such as
connectrpc-build), select the buffa backend — presence checks go through
MessageField, enum accesses through EnumValue::to_i32, and type paths
keep verbatim proto names (UUID stays UUID):
new
.file_descriptor_set_path?
.backend
// Optional: abort the build instead of skipping messages the
// generator cannot cover (for consumers with no runtime Validator).
.fail_on_runtime_only
.compile?;
Pairing the buffa backend with default-features = false on
prost-protovalidate yields build-time validation with no prost-reflect
and no CEL anywhere in the runtime dependency graph.
Alternatively, runtime_bridge(true) (instead of fail_on_runtime_only)
routes CEL-bearing and other uncoverable messages through an embedded
runtime engine: they get a generated impl Validate that encodes the
message to wire bytes and validates it with the same conformance-tested
Validator the runtime path uses — full buf.validate coverage, uniform
msg.validate() across the schema. The routed messages require
prost-protovalidate with reflect/cel enabled, and the generated code
must be included from OUT_DIR:
new
.file_descriptor_set_path?
.backend
.runtime_bridge
.compile?;
Supported Rules
- Scalar: bool const, numeric comparisons (gt/gte/lt/lte/const/in/not_in), string (min_len/max_len/pattern/prefix/suffix/contains/in/not_in/well-known formats), bytes (min_len/max_len/pattern/prefix/suffix/in/not_in/well-known formats), enum (const/in/not_in/defined_only)
- Repeated: min_items/max_items, per-item scalar constraints
- Map: min_pairs/max_pairs, per-key and per-value scalar constraints
- Message: required, nested recursive validation
- Duration/Timestamp: comparison constraints (gt/gte/lt/lte/const)
- FieldMask: const, in/not_in with prefix path matching
- Any: type_url in/not_in
- WKT wrappers:
google.protobuf.*Valuetypes unwrapped to inner scalar rules - Oneof: required (must have a variant set)
- Ignore:
IGNORE_ALWAYS,IGNORE_IF_ZERO_VALUE,IGNORE_UNSPECIFIEDwith presence semantics
Limitations
- CEL expressions are not compiled to Rust at build time. Messages with CEL rules (field-level or message-level) are skipped with a
cargo:warning, fail the build whenfail_on_runtime_only(true)is set, or — on the buffa backend — are delegated to the embedded runtime engine withruntime_bridge(true)(interpreter-speed, pullsreflect/celinto the consumer). - Predefined CEL constraints (custom rules) follow the same routing as CEL expressions.
- Nested runtime-only dependencies (for example, nested CEL or unsupported nested rules) route the parent message the same way, to prevent partial validation.
runtime_bridgerequiresOUT_DIRinclusion — the generated bridge embeds the descriptor set viainclude_bytes!(concat!(env!("OUT_DIR"), …)), so do not combine it with anout_dir()override for code that is subsequently compiled.
License
MIT OR Apache-2.0