Static-codegen protovalidate for the buffa Rust protobuf runtime.
Annotate your .proto messages with (buf.validate.*) rules; a codegen plugin emits pure-Rust impl Validate blocks per message. Handlers call req.validate()? at entry (or use the #[connect_impl] macro to do it automatically on every handler in a service impl).
For what the rules mean, the full rule catalogue, CEL semantics, and design docs, read the upstream project — this crate intentionally does not duplicate that material:
- Docs: https://buf.build/docs/protovalidate/
- Rule catalogue: https://buf.build/docs/protovalidate/schemas/standard-rules/
- Custom / predefined rules: https://buf.build/docs/protovalidate/schemas/custom-rules/
- Source of truth (
validate.proto): https://github.com/bufbuild/protovalidate/blob/main/proto/protovalidate/buf/validate/validate.proto
Conformance: 2872 / 2872 (100%) against the upstream protovalidate-conformance harness, covering proto2, proto3, and editions 2023.
Existing Rust implementations of protovalidate (prost-protovalidate, protocheck, protify) all target prost. buffa has a different runtime model — two-tier owned/borrowed types with zero-copy views, and static generated types rather than descriptor-driven dynamic messages — so prost-based validators are incompatible. This repo fills that gap.
Compared to reflection-based implementations, the codegen approach has two characteristics:
- No runtime descriptor lookup. Every
validate()is a direct struct field walk that LLVM can inline. - Schema-aware compile errors. Rule / field type mismatches, malformed
message.oneofspecs, and CEL expressions that reference non-existent fields are surfaced at codegen time rather than at first-call.
| Crate | Purpose |
|---|---|
protovalidate-buffa |
Runtime library: Validate trait, structured ValidationError (with typed compile_error / runtime_error slots), Violation / FieldPath, rule helpers, CelScalar widening trait + Duration/Timestamp helpers, Connect error adapter. CEL rules are transpiled to native Rust at codegen time, so the runtime carries no interpreter. |
protovalidate-buffa-macros |
#[connect_impl] attribute macro — inserts request validation at the top of every handler in a service impl block. Re-exported from the runtime crate. |
protoc-gen-protovalidate-buffa |
Codegen plugin. Reads (buf.validate.*) extensions off descriptors via buffa's ExtensionSet, emits impl Validate for Foo blocks. Wire into buf.gen.yaml. |
protovalidate-buffa-protos |
Compiled Rust for buf/validate/validate.proto (vendored under proto/). Consumed by the codegen plugin. |
protovalidate-buffa-conformance also lives in this workspace but is private (publish = false) — see its README for the conformance test-run flow.
This workspace currently targets buffa 0.8, connectrpc 0.8, and Rust 1.88+ (edition 2024).
The emitted validators reference buffa's generated-code shape directly (field placement, map and view types), so the plugin and your buffa-build output must agree on the buffa minor version. buffa is pre-1.0 and treats minor bumps as breaking, so upgrading buffa generally means upgrading this crate in lockstep.
buffa 0.9 is intentionally not adopted yet: connectrpc 0.8 still depends on buffa ^0.8, so moving this workspace to 0.9 would link two incompatible buffa versions and break the default connect feature for downstream handlers. This crate will move to buffa 0.9 once connectrpc does.
Every rule family in the upstream standard-rules catalogue plus predefined rules is implemented — that's what the 2872 / 2872 conformance number above is measuring. See the upstream docs for semantics; this repo doesn't maintain a parallel list.
For the proto-annotation side (which rules exist, how to combine them, CEL syntax), follow the upstream protovalidate quick start. The Rust-specific bits are:
# Install the plugin
cargo install --git https://github.com/mathematic-inc/protovalidate-buffa protoc-gen-protovalidate-buffaAdd to your buf.gen.yaml:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allAnnotate a proto (see upstream for the full rule vocabulary):
syntax = "proto3";
import "buf/validate/validate.proto";
message CreateUserRequest {
string email = 1 [(buf.validate.field).string = { min_len: 5, max_len: 254, email: true }];
int32 age = 3 [(buf.validate.field).int32 = { gte: 13, lte: 150 }];
}Use in a Connect handler:
use protovalidate_buffa::Validate;
#[protovalidate_buffa::connect_impl]
impl UserService for UserServiceImpl {
async fn create_user(
&self,
ctx: Context,
request: connectrpc::ServiceRequest<'_, pb::CreateUserRequest>,
) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
// #[connect_impl] validates the request here automatically.
// Body only sees already-validated requests.
}
}Validate::validate returns Result<(), ValidationError>:
pub struct ValidationError {
pub violations: Vec<Violation>,
pub compile_error: Option<String>, // schema mismatch detected at codegen time
pub runtime_error: Option<String>, // rule precondition failed (e.g. non-UTF-8 bytes under `pattern`)
}Match on the typed fields rather than stringly-typed rule-id prefixes. Violation / FieldPath mirror the upstream proto shape — see those message definitions for field semantics. The connect feature provides ValidationError::into_connect_error mapping to InvalidArgument.
See crates/protovalidate-buffa-conformance/README.md for how to build the dispatch binary and drive the upstream harness locally. CI runs cargo clippy --workspace --all-targets -- -D warnings and cargo test --workspace on every push; conformance is currently a local-only / pre-release check.
Dual-licensed under Apache-2.0 or MIT at your option.