Validation for Protocol Buffer messages using buf.validate rules — a runtime engine built on prost-reflect, plus a compile-time validation path via prost-protovalidate-build targeting both prost and buffa message types.
By default, prost-protovalidate evaluates buf.validate constraints (including Common Expression Language / CEL expressions) at runtime through prost-reflect, enforcing rules directly from your single source of truth — your .proto files. For messages with standard (non-CEL) rules, prost-protovalidate-build generates impl Validate at compile time so validation runs through monomorphized direct field access — the fastest path at runtime: no reflection, no CEL interpreter on the hot path. On the buffa backend, CEL-bearing messages can additionally be routed through the runtime engine (runtime_bridge), giving buffa types the full buf.validate rule surface from one library.
- Compile-time validation (fastest path at runtime) —
prost-protovalidate-buildemitsimpl Validatefor messages with standard-only rules; noprost-reflecttranscoding, no CEL interpreter, monomorphized direct field access. Combined withdefault-features = falseonprost-protovalidate, the entirecel/chrono/pastey/thiserror1.x subtree drops out of your build. Trades binary size and build time for hot-path speed; messages with CEL automatically fall back to the runtimeValidator(with acargo:warning=diagnostic, never silently skipped). - Runtime validation with CEL — Leverages
prost-reflectto dynamically inspect message fields and evaluate complex validation rules, including arbitrary CEL expressions, at runtime. - CEL Evaluation — Fully supports compiling and evaluating Common Expression Language (CEL) conditions for cross-field or complex constraints.
- Proto as single source of truth — Define validation rules inside
.protofiles usingbuf.validateand enforce them automatically in Rust. - One library for
prostandbuffa— the samebuf.validaterules, the same violation output, whichever protobuf runtime generates your types. SelectBuilder::backend(Backend::Buffa)for buffa (buffa-build, or a wrapper such asconnectrpc-build). - Two engines, one rule source — the runtime
Validatorand the compile-time generator both consume the canonical rule ids/messages inprost-protovalidate-types::rules_meta, so they cannot drift; a descriptor-driven parity sweep asserts identical violations across both engines. - Harness-verified conformance — the runtime engine passes the full upstream protovalidate conformance suite: 2872/2872 tests (protovalidate v1.2.2, 0 expected failures).
- Fail-closed or full-coverage routing —
fail_on_runtime_only(true)turns any rule the generator cannot cover into a hard build error (for services that ship no runtime validator); on buffa,runtime_bridge(true)instead delegates those messages to the embedded runtime engine for full CEL coverage. - Three footprint tiers — full (
cel, default) ⊃reflect(runtimeValidatorwithout CEL) ⊃ slim (default-features = false; build-time validation only, noprost-reflectin the graph). - gRPC integration — optional
tonicfeature mapsValidationErrortotonic::Status, andtonic-typesattachesgoogle.rpc.BadRequestfield-level details. - Edition 2023 support — Normalizes Edition 2023 descriptors (including
DELIMITEDgroup encoding) soprost-reflect0.16 handles them correctly. - Modular Crates — Clear separation between raw protobuf types (
prost-protovalidate-types) and the runtime validation engine (prost-protovalidate).
| Crate | Purpose | Cargo section |
|---|---|---|
| prost-protovalidate-types | buf.validate proto types with prost and prost-reflect support; hosts rules_meta (canonical rule ids/messages for both engines) |
[dependencies] |
| prost-protovalidate | Runtime validation engine (CEL parsing, constraint evaluation) | [dependencies] |
| prost-protovalidate-build | Compile-time code generator for validation | [build-dependencies] |
Add the dependencies to your Cargo.toml:
[dependencies]
prost = "0.14"
prost-protovalidate = "0.6"| Feature | Default | Description |
|---|---|---|
cel |
Yes | CEL expression evaluation and chrono time support (implies reflect) |
reflect |
Yes (via cel) |
Runtime reflection: the descriptor-driven Validator, validation filters, Violation rule-path hydration, and the bridge module used by runtime_bridge generated code |
tonic |
No | gRPC integration: From<ValidationError> for tonic::Status and a ValidateRequest extension trait (req.validate_inner()? in handlers) |
tonic-types |
No | Implies tonic. Attaches google.rpc.BadRequest details to validation-failure statuses so clients can parse field-level errors without scraping the message string |
To keep the runtime Validator but disable CEL (removes cel, chrono,
pastey, and thiserror 1.x transitive deps):
[dependencies]
prost-protovalidate = { version = "0.6", default-features = false, features = ["reflect"] }Without cel, standard rules (range checks, string constraints, format
validators, etc.) work normally. Messages with CEL expressions or predefined
CEL rules will produce a CompilationError at validation time.
For build-time-only validation (generated impl Validate from
prost-protovalidate-build), drop features entirely — the slim build keeps
only the Validate trait, Violation/ValidationError, and the
validators helpers, with no prost-reflect in the dependency graph:
[dependencies]
prost-protovalidate = { version = "0.6", default-features = false }Annotate your .proto files with buf.validate rules:
syntax = "proto3";
import "buf/validate/validate.proto";
message CreateUserRequest {
string name = 1 [(buf.validate.field).string.min_len = 1];
int32 age = 2 [(buf.validate.field).int32.gte = 18];
}In your Rust code, run the validator against a generated prost message using the provided validate shortcut:
use prost_protovalidate::validate;
let request = CreateUserRequest {
name: "Alice".to_string(),
age: 25,
};
match validate(&request) {
Ok(_) => println!("Validation passed!"),
Err(e) => println!("Validation failed: {}", e),
}Or cache a Validator instance if you need to perform multiple validations efficiently:
use prost_protovalidate::Validator;
// Creating a Validator parses and caches rules under the hood
let validator = Validator::new();
validator.validate(&request)?;| prost-protovalidate | prost | prost-reflect | MSRV |
|---|---|---|---|
| 0.6.x | 0.14 | 0.16 | 1.86 |
| 0.5.x | 0.14 | 0.16 | 1.86 |
| 0.4.x | 0.14 | 0.16 | 1.86 |
Three validation modes cover different use cases:
For messages with only standard rules (no CEL). Validators are generated
at compile time and run through monomorphized direct field access — no
prost-reflect transcoding, no CEL interpreter, no dynamic dispatch on
the hot path. Combined with default-features = false on
prost-protovalidate, the entire cel / chrono / pastey / thiserror
1.x subtree drops out of your build. Requires prost-protovalidate-build in
[build-dependencies].
Generated code targets prost message types by default; select
Builder::backend(Backend::Buffa) when your types come from
buffa (buffa-build or a wrapper such as
connectrpc-build). With Builder::fail_on_runtime_only(true), any message
the generator cannot cover fails the build instead of falling back — for
consumers that ship no runtime validator at all.
On the buffa backend, Builder::runtime_bridge(true) is the third option:
messages the generator cannot cover (CEL rules, unsupported shapes) receive a
generated impl Validate that encodes the message to wire bytes and validates
it through an embedded runtime engine — so every message gets a uniform
msg.validate(), standard rules inline and everything else through the same
conformance-tested Validator. The routed messages require
prost-protovalidate with reflect/cel enabled and run at
interpreter speed; keep fail_on_runtime_only when you want the slim,
CEL-free guarantee instead.
use prost_protovalidate::Validate;
msg.validate()?;For messages with CEL expressions or mixed rules. Uses prost-reflect
dynamic dispatch and the CEL interpreter at runtime.
use prost_protovalidate::Validator;
let validator = Validator::new();
validator.validate(&msg)?;For services that don't use CEL. Disabling the cel feature removes cel,
chrono, pastey, and transitive thiserror 1.x deps. Pairs naturally with
compile-time validators above for a CEL-free dependency tree end to end.
[dependencies]
prost-protovalidate = { version = "0.4", default-features = false }Standard rules work normally; messages with CEL rules produce a
CompilationError.
| Proto rules | Validate trait generated? |
How to validate |
|---|---|---|
| Standard only (min_len, gte, email, etc.) | Yes | msg.validate() (compile-time, fastest) |
| Time-relative timestamp (lt_now, gt_now, within) | Yes (reads SystemTime::now()) |
msg.validate() (compile-time, fastest) |
repeated.unique on float/double |
Yes (canonical IEEE-754 bits) | msg.validate() (compile-time, fastest) |
| CEL only (expressions) | No¹ | validator.validate(&msg) (runtime) |
| Mixed (standard + CEL) | No¹ | validator.validate(&msg) (runtime) |
| Nested runtime-only dependencies | No¹ | validator.validate(&msg) (runtime) |
| No rules | No | — |
¹ On the buffa backend with runtime_bridge(true), these messages do get a
generated impl Validate that delegates to the embedded runtime engine, so
msg.validate() works uniformly across the whole schema.
For messages with time-relative timestamp rules, the generated
Validate::validate(&self) impl reads SystemTime::now() once per call —
identical to the runtime Validator's default behaviour. Tests that need
a deterministic clock must use the runtime Validator and override
now_fn on ValidatorOptions; the trait signature has no place to inject
a clock source.
Enable the tonic feature to map validation errors to gRPC statuses:
[dependencies]
prost-protovalidate = { version = "0.6", features = ["tonic"] }
tonic = "0.14"In your service handler:
use prost_protovalidate::tonic::ValidateRequest;
#[tonic::async_trait]
impl my_proto::greeter_server::Greeter for GreeterImpl {
async fn say_hello(
&self,
req: tonic::Request<my_proto::HelloRequest>,
) -> Result<tonic::Response<my_proto::HelloReply>, tonic::Status> {
req.validate_inner()?;
// ... handler body ...
}
}ValidateRequest is implemented for unary and server-streaming handlers
(tonic::Request<T>). For client-streaming and bidirectional handlers
(tonic::Request<tonic::Streaming<T>>), validate each message inside the
per-message loop: msg.validate().map_err(tonic::Status::from)?.
ValidationError maps to Code::InvalidArgument. CompilationError /
RuntimeError (and the unified Error::Compilation / Error::Runtime
variants) map to Code::Internal with a fixed, generic message — the
underlying cause strings can contain proto field names and CEL internals
that should not be exposed to untrusted clients. Log the original error
server-side before converting if you need the full cause.
Enabling the additional tonic-types feature attaches a google.rpc.BadRequest
detail to validation-failure statuses, with one FieldViolation per
violation, so clients can parse field-level errors programmatically:
[dependencies]
prost-protovalidate = { version = "0.6", features = ["tonic-types"] }Add prost-protovalidate-build to your build dependencies:
[dependencies]
prost = "0.14"
prost-protovalidate = "0.6"
[build-dependencies]
prost-build = "0.14"
prost-protovalidate-build = "0.6"In your build.rs:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let descriptor_path = std::path::PathBuf::from(std::env::var("OUT_DIR")?)
.join("file_descriptor_set.bin");
prost_build::Config::new()
.file_descriptor_set_path(&descriptor_path)
.compile_protos(&["proto/service.proto"], &["proto/"])?;
prost_protovalidate_build::Builder::new()
.file_descriptor_set_path(&descriptor_path)?
.compile()?;
Ok(())
}Include the generated validators alongside your prost-generated code:
include!(concat!(env!("OUT_DIR"), "/validate_impl.rs"));Messages the generator cannot cover (CEL or predefined CEL rules, and messages that transitively depend on runtime-only nested validation) are routed by one of three modes:
- Default — skipped with a
cargo:warningexplaining why; validate them with the runtimeValidator. fail_on_runtime_only(true)— the build fails instead, so a rule can never be silently dropped (for consumers that ship no runtime validator).runtime_bridge(true)(buffa backend) — a generatedimpl Validatedelegates them to an embedded runtime engine, giving the whole schema a uniformmsg.validate()including full CEL support.
Building from source requires protoc (the prost-protovalidate-types build
script compiles the vendored buf.validate schema). Install it via your
package manager or set the PROTOC environment variable to the binary path.
make pre-commit # fmt-check + lint + test (run before committing)
make conformance # conformance suite (requires Go)Full conformance with the bufbuild protovalidate test suite (v1.2.2): 2872/2872 tests pass (0 expected failures).
Conformance uses a pinned upstream harness from
github.com/bufbuild/protovalidate/tools/protovalidate-conformance.
Pinned versions are defined in the repository root Makefile:
PROTOVALIDATE_TOOLS_VERSIONPROTOVALIDATE_SCHEMA_REF
- Bump
PROTOVALIDATE_TOOLS_VERSIONandPROTOVALIDATE_SCHEMA_REFinMakefile. - Sync
validate.protousing the workflow incrates/prost-protovalidate-types/SYNC.md. - Run conformance and update
expected_failures.yamlwith any new failures:make conformance
- Run regression tests:
cargo test --all-features
- Commit version bump and
expected_failures.yamlchanges together.
MIT OR Apache-2.0