Releases: mathematic-inc/protovalidate-buffa
Release list
protoc-gen-protovalidate-buffa-v0.5.1
protoc-gen-protovalidate-buffa v0.5.1
This patch release squashes a codegen bug that caused generated validators to fail to compile when an optional string field (or an editions 2023 field_presence = EXPLICIT field) carried a string.pattern rule — exactly the kind of silent failure that's hardest to track down. If you have any such fields in your schema, upgrade immediately. Everyone else gets a free correctness improvement and a regression test that'll keep it fixed forever.
Conformance remains at 2872 / 2872 (100%) across proto2, proto3, and editions 2023.
🐛 Bug Fixes
Fix borrow in optional string pattern check (#21)
What broke. An explicit-presence string field — written as optional string in proto3, or using field_presence = EXPLICIT in editions — is lowered to an owned let v: String binding before the shared string-rule emitter runs. The string.pattern check was then passing that v directly to Regex::is_match, which expects &str. An owned String does not coerce in argument position, so the generated code wouldn't compile:
error[E0308]: mismatched types
--> gen/protovalidate/my_message.rs:42:34
|
| PATTERN.is_match(v)
| ^ expected `&str`, found `String`
Why conformance didn't catch it. The upstream protovalidate-conformance suite tests string.pattern only on plain (implicit-presence) fields, where the binding is &String and auto-deref coerces correctly. The optional path never exercised this branch.
The fix. The emitter now passes v.as_str(), which works whether v is an owned String (optional path) or a &String (oneof path). A dedicated render-based regression test covering both paths was added to make sure this can't regress silently again.
Who is affected. Any schema with a field declared optional string (proto3) or field_presence = EXPLICIT (editions) that also has a string.pattern rule. If your schema has such a field, the generated validator would not have compiled at all — so you would already know. After upgrading and re-running buf generate, it will compile and validate correctly.
Proto example:
syntax = "proto3";
import "buf/validate/validate.proto";
message SlugRequest {
// optional + pattern: previously generated non-compiling Rust
optional string slug = 1 [(buf.validate.field).string = {
pattern: "^[a-z0-9-]+$"
min_len: 1
max_len: 64
}];
}Generated code now compiles:
// protovalidate-buffa emits this — v0.5.1 correctly borrows the owned binding
if let Some(v) = &self.slug {
let v: String = v.clone(); // explicit-presence path
if !SLUG_PATTERN.is_match(v.as_str()) { // <-- .as_str() is the fix
violations.push(/* ... */);
}
}Error handling (unchanged from v0.5.0 — matching on typed fields, not stringly-typed prefixes):
use protovalidate_buffa::{Validate, ValidationError};
match req.validate() {
Ok(()) => { /* proceed */ }
Err(ValidationError { violations, compile_error: None, runtime_error: None }) => {
// Field constraint violations — surface these to the caller
for v in &violations {
eprintln!("violation on {}: {}", v.field_path, v.message);
}
}
Err(ValidationError { compile_error: Some(e), .. }) => {
// Schema mismatch caught at codegen time — this is a bug in the .proto
panic!("codegen schema error: {e}");
}
Err(ValidationError { runtime_error: Some(e), .. }) => {
// Rule precondition failed at runtime (e.g. non-UTF-8 bytes under `pattern`)
eprintln!("runtime validation error: {e}");
}
}Installation
Runtime library (protovalidate-buffa)
[dependencies]
protovalidate-buffa = "0.5.1"Codegen plugin (protoc-gen-protovalidate-buffa)
cargo install \
--git https://github.com/mathematic-inc/protovalidate-buffa \
--tag protoc-gen-protovalidate-buffa-v0.5.1 \
protoc-gen-protovalidate-buffaThen re-run buf generate to pick up the fixed output. No changes to your buf.gen.yaml or .proto files are required.
v0.5.1
protovalidate-buffa v0.5.1
#[connect_impl] now speaks fluent Connect 0.7 — and still hits 2872 / 2872 (100%) on the upstream conformance suite.
This patch release rounds out the Connect integration story. The #[connect_impl] macro previously understood only buffa's OwnedView<T> handler signatures; it now recognises connectrpc::ServiceRequest<'_, T> as well, so teams that have adopted the Connect 0.7 owned-message wrapper get automatic validation out of the box — no per-handler boilerplate, no forgetting a req.validate()?.
🐛 Bug Fixes
#[connect_impl] validates Connect ServiceRequest handlers
Prior to this release, applying #[connect_impl] to a service impl that used Connect 0.7's ServiceRequest<'_, T> parameter type silently skipped those handlers — the macro only injected validation when it spotted an OwnedView<T>. Any handler written in the newer style received no automatic validation at all.
The macro now detects both shapes:
| Handler signature | Validated? |
|---|---|
request: OwnedView<pb::FooView<'static>> |
✅ (was already working) |
request: connectrpc::ServiceRequest<'_, pb::Foo> |
✅ (fixed in v0.5.1) |
Both types expose to_owned_message(), so the injected code is uniform and zero-surprise:
// Apply once at the impl level — every handler is covered, now and in future.
#[protovalidate_buffa::connect_impl]
impl UserService for UserServiceImpl {
async fn create_user(
&self,
ctx: Context,
// Connect 0.7 ServiceRequest — now fully validated automatically.
request: connectrpc::ServiceRequest<'_, pb::CreateUserRequest>,
) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
// Validation runs before this line.
// Invalid requests are rejected as InvalidArgument before reaching here.
let req = request.into_message();
// ...
}
}Validation failures surface as a structured ConnectError::invalid_argument carrying the full set of rule violations, powered by ValidationError::into_connect_error. The typed error fields let you distinguish failure modes cleanly:
match msg.validate() {
Ok(()) => { /* proceed */ }
Err(e) if e.compile_error.is_some() => {
// Schema mismatch caught at codegen time; this is a bug in the .proto or plugin config.
tracing::error!(compile_error = %e.compile_error.as_deref().unwrap_or(""));
}
Err(e) if e.runtime_error.is_some() => {
// Rule precondition couldn't be evaluated (e.g. non-UTF-8 bytes under `pattern`).
tracing::warn!(runtime_error = %e.runtime_error.as_deref().unwrap_or(""));
}
Err(e) => {
// The common case: one or more field-level rule violations.
for v in &e.violations {
tracing::debug!(field = %v.field_path, message = %v.message);
}
}
}Conformance
2872 / 2872 (100%) — all cases in the upstream protovalidate-conformance harness pass, covering proto2, proto3, and editions 2023. Nothing regressed.
Installation
Library consumers
[dependencies]
protovalidate-buffa = "0.5.1"Enable the connect feature (on by default) to get ValidationError::into_connect_error and the #[connect_impl] macro.
Codegen plugin
cargo install \
--git https://github.com/mathematic-inc/protovalidate-buffa \
--tag v0.5.1 \
protoc-gen-protovalidate-buffaThen wire it into your buf.gen.yaml:
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allDual-licensed Apache-2.0 / MIT.
v0.5.0
protovalidate-buffa v0.5.0
protovalidate-buffa v0.5.0 lands three important correctness fixes that make the codegen plugin robust across real-world proto schemas: keyword-named fields now produce valid Rust instead of panicking at generation time, the optional Connect integration no longer drags in streaming and compression crates you never asked for, and the whole workspace has been updated to the buffa 0.7 line. The 100% conformance score — 2872 / 2872 cases across proto2, proto3, and editions 2023 — remains intact throughout.
🐛 Bug Fixes
Keyword-named fields and oneofs no longer break codegen
The plugin was building field accessors (self.<field>) directly from raw proto names. Any field or oneof whose name is a Rust keyword caused one of three failure modes:
format_ident!("type")emits the raw invalid tokenself.type—syn/prettypleaserefuse to parse it.parse_str::<Ident>("type")returnsErrand fails the render pipeline.- Keywords that can't be raw identifiers (
self,super,crate,Self) caused an outrightIdent::newpanic at codegen time.
The fix routes every proto-name-to-ident conversion through a shared field_ident helper that delegates to buffa_codegen::idents::make_field_ident — the same function buffa itself uses when naming struct fields. Emitted accessors now match the generated message types by construction:
// proto
message Request {
string type = 1 [(buf.validate.field).string.min_len = 1];
string self = 2 [(buf.validate.field).string.min_len = 1];
}// generated (before this fix: compile error / panic)
impl Validate for Request {
fn validate(&self) -> Result<(), ValidationError> {
// raw-able keyword → r#type
if self.r#type.len() < 1 { /* ... */ }
// non-raw-able keyword → suffixed
if self.self_.len() < 1 { /* ... */ }
Ok(())
}
}Regression tests covering keyword fields, keyword-named oneofs, and CEL rules on keyword-named fields are now part of the plugin's integration test suite.
This is a breaking change if you were working around the bug by renaming proto fields — you can remove those workarounds now.
Connect feature no longer pulls in streaming and compression crates
The optional connect feature only ever needed ConnectError and ErrorCode from connectrpc. By default, connectrpc previously enabled its streaming and compression feature flags, which added async-compression and related transitive dependencies to your build even when you just wanted req.validate()? → InvalidArgument. Those are now gone. Enabling the connect feature is leaner than it used to be.
⚡ Improvements
buffa 0.7
The workspace — runtime, macros, protos, plugin, and conformance harness — is now on the buffa 0.7 line. connectrpc has been moved to its matching 0.7 release to keep the transitive dependency graph consistent.
Conformance
2872 / 2872 (100%) against the upstream protovalidate-conformance suite, covering proto2, proto3, and editions 2023. Every rule family in the standard-rules catalogue and predefined-rules support is covered.
Error handling reminder
Match on the typed ValidationError fields, not on rule_id prefixes:
match req.validate() {
Ok(()) => { /* proceed */ }
Err(e) if e.compile_error.is_some() => {
// Schema-level mismatch caught at codegen time — this is a bug
// in your proto annotations or plugin version. Log and panic in dev.
panic!("protovalidate compile error: {}", e.compile_error.unwrap());
}
Err(e) if e.runtime_error.is_some() => {
// Rule precondition failed at runtime (e.g. bytes.pattern on non-UTF-8).
return Err(e.into_connect_error());
}
Err(e) => {
// The common case: one or more field violations.
return Err(e.into_connect_error()); // → InvalidArgument
}
}Installation
Library consumers — add to Cargo.toml:
protovalidate-buffa = "0.5.0"Codegen plugin — install from source:
cargo install \
--git https://github.com/mathematic-inc/protovalidate-buffa \
--tag v0.5.0 \
protoc-gen-protovalidate-buffaThen wire it into your buf.gen.yaml:
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allprotovalidate-buffa-protos-v0.5.0
protovalidate-buffa-protos v0.5.0
This release ships three targeted fixes that make the codegen plugin more
robust in real-world proto schemas: proto fields and oneofs named with Rust
keywords now generate valid code instead of panicking, the connect feature
pulls in substantially fewer transitive dependencies, and the whole workspace
moves to the buffa 0.7 line. The 100% conformance record stands —
2872 / 2872 cases across proto2, proto3, and editions 2023 — and now the
plugin can handle whatever names your protos throw at it.
🐛 Bug Fixes
Rust keyword field and oneof names no longer break codegen
The plugin previously built field accessors (self.<field>) directly from raw
proto names. Any field or oneof named with a Rust keyword — type, match,
use, or even self — caused format_ident! to emit invalid syntax,
parse_str::<Ident> to return an error, or Ident::new to panic outright.
All name-to-ident conversions in the emitter now route through a shared
field_ident helper that delegates to buffa_codegen::idents::make_field_ident
— the exact same function buffa uses when naming struct fields. This guarantees
that generated accessors always match the buffa-generated struct layout, even
on keyword names:
// These used to generate broken Rust. Now they work.
message FilterRequest {
string type = 1 [(buf.validate.field).string.min_len = 1];
string match = 2 [(buf.validate.field).string.min_len = 1];
}Generated accessors now emit self.r#type and self.r#match (or the
_-suffix form for the rare non-raw-able keywords), matching the buffa struct
fields exactly. A regression test suite for the full emit::render pipeline
on keyword names is included.
connect feature no longer pulls streaming and compression dependencies
The optional Connect integration only needs ConnectError and ErrorCode.
connectrpc's default features were previously enabled, which dragged in
streaming and compression transitive dependencies unconditionally whenever the
connect feature was active.
connectrpc is now declared with default-features = false, so enabling
connect brings in only what the validation→Connect-error mapping actually
uses. If you match on ValidationError in a Connect handler, the ergonomics
are unchanged:
use protovalidate_buffa::{ValidationError, Validate};
fn handle(req: &MyRequest) -> Result<(), ConnectError> {
req.validate().map_err(|e| {
// Match on the typed fields, not rule_id string prefixes.
if e.compile_error.is_some() {
// Schema-level mismatch caught at codegen — should not reach prod.
ConnectError::new(Code::Internal, "validation schema error")
} else if e.runtime_error.is_some() {
// Rule precondition failed at runtime (e.g. non-UTF-8 under `pattern`).
ConnectError::new(Code::Internal, "validation runtime error")
} else {
// The common path: field rule failures.
e.into_connect_error()
}
})
}Or just use #[connect_impl] and let the macro handle it for every handler
at once:
#[protovalidate_buffa::connect_impl]
impl MyService for MyServiceImpl {
async fn create_user(
&self,
ctx: Context,
request: OwnedView<pb::CreateUserRequestView<'static>>,
) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
// req.validate()? is inserted automatically — body only sees
// already-validated requests.
}
}⬆️ Updated Dependencies
- buffa → 0.7 across the workspace (
buffa,buffa-build,buffa-codegen). - connectrpc → 0.7 (aligned with the buffa 0.7 transitive requirement,
default features disabled).
Conformance
2872 / 2872 (100%) — every case in the upstream
protovalidate-conformance
suite passes, covering proto2, proto3, and editions 2023.
Installation
Library consumers — add to Cargo.toml:
[dependencies]
protovalidate-buffa = "0.5.0"Codegen plugin — install from source:
cargo install \
--git https://github.com/mathematic-inc/protovalidate-buffa \
--tag protovalidate-buffa-protos-v0.5.0 \
protoc-gen-protovalidate-buffaThen wire it into buf.gen.yaml:
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allprotovalidate-buffa-macros-v0.3.1
protovalidate-buffa-macros v0.3.1
#[connect_impl] just levelled up. This patch release teaches the macro to recognise Connect 0.7's ServiceRequest<'_, T> request parameters alongside the existing OwnedView<T> support — meaning your service impls get automatic, zero-boilerplate proto validation regardless of which request wrapper your handlers use. One attribute, every handler covered, no per-method discipline required. The full upstream conformance suite remains at a perfect 2872 / 2872 (100%) across proto2, proto3, and editions 2023.
🐛 Bug Fixes
#[connect_impl] now validates ServiceRequest handlers
Prior to this release, the macro only injected validate()? into handlers whose request parameter was typed as OwnedView<_>. Handlers using Connect 0.7's connectrpc::ServiceRequest<'_, T> wrapper were silently skipped — no validation, no error, just a gap in your safety net.
Before (validation was not injected):
#[connect_impl]
impl UserService for UserServiceImpl {
async fn create_user(
&self,
ctx: Context,
request: connectrpc::ServiceRequest<'_, pb::CreateUserRequest>,
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Previously ignored by #[connect_impl] — no validate()!
) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
// User code ran even on invalid requests. Oops.
}
}After (validation is injected automatically):
#[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] now validates the request here automatically.
// Body only sees already-validated requests — for both OwnedView and ServiceRequest.
}
}The macro now recognises both OwnedView<_> and ServiceRequest<'_, _> as handler request parameters and injects validation for either. Non-handler methods that lack a recognised request parameter continue to be left untouched.
If you match on validation errors, remember to use the typed fields — not stringly-typed rule ID prefixes:
match req.validate() {
Ok(()) => { /* proceed */ }
Err(e) if e.compile_error.is_some() => {
// Schema mismatch caught at codegen time — should not reach production
panic!("codegen schema error: {:?}", e.compile_error);
}
Err(e) if e.runtime_error.is_some() => {
// Rule precondition failed at runtime (e.g. non-UTF-8 bytes under `pattern`)
return Err(e.into_connect_error());
}
Err(e) => {
// One or more field violations
return Err(e.into_connect_error());
}
}Getting Started
Library consumers
Add to your Cargo.toml:
[dependencies]
protovalidate-buffa = "0.5.1" # re-exports #[connect_impl] and the full runtimeThe #[connect_impl] macro is gated behind the connect feature (enabled by default). If you want the macros crate directly:
[dependencies]
protovalidate-buffa-macros = "0.3.1"Codegen plugin
cargo install \
--git https://github.com/mathematic-inc/protovalidate-buffa \
--tag protovalidate-buffa-macros-v0.3.1 \
protoc-gen-protovalidate-buffaThen wire it into buf.gen.yaml:
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allHappy validating! 🛡️
protoc-gen-protovalidate-buffa-v0.5.0
protoc-gen-protovalidate-buffa v0.5.0
This release hardens the codegen plugin against real-world proto schemas, cleans up
the optional Connect dependency, and upgrades the entire workspace to buffa 0.7 — all
while keeping the 2872 / 2872 (100%) upstream conformance score untouched across
proto2, proto3, and editions 2023. If you have proto fields or oneofs named after Rust
keywords (type, self, super, Self, crate, …), this release fixes a class of
panics and invalid-code-generation bugs that would have silently broken your validator
output. Upgrade is strongly recommended.
🐛 Bug Fixes
Rust keyword field and oneof names no longer break codegen
The plugin previously constructed field accessors by passing raw proto field names
directly to format_ident! / Ident::new / parse_str::<Ident>. Any field or
oneof whose proto name happened to be a Rust keyword crashed or produced unparseable
output:
format_ident!("type")— emittedself.typewhichsynrejects.parse_str::<Ident>("self")— returnedErr, halting generation.Ident::new_raw("self")— panicked becauseselfcannot be a raw identifier.
All accessor construction is now routed through a shared field_ident helper that
delegates to buffa_codegen::idents::make_field_ident — the same function buffa uses
when generating the message structs. This guarantees that emitted accessors always match
the generated types by construction:
| Proto field name | Generated accessor |
|---|---|
type |
self.r#type |
self |
self.self_ |
crate |
self.crate_ |
// Previously crashed the plugin. Now works.
message Request {
string type = 1 [(buf.validate.field).string.min_len = 1];
}Regression tests covering required fields, CEL rules, and required oneofs with
keyword names are included to pin this behaviour permanently.
Connect feature no longer pulls in streaming/compression transitive deps
Enabling the connect feature only requires ConnectError and ErrorCode from the
connectrpc crate. Default features (which include streaming and compression support)
were previously activated unconditionally, bloating the dependency graph for users who
only needed the InvalidArgument mapping. default-features = false is now set on
the connectrpc dependency, so you get exactly the error types and nothing more.
⚡ Improvements
buffa 0.7
The workspace now tracks the buffa 0.7 line across all crates (buffa, buffa-build,
buffa-codegen, connectrpc). If you are already on buffa 0.7 in your own project,
all transitive dependencies now resolve cleanly to a single version with no duplication.
Error handling reference
Match on the typed ValidationError fields — never on rule_id string prefixes:
use protovalidate_buffa::{Validate, ValidationError};
match req.validate() {
Ok(()) => { /* all rules passed */ }
Err(ValidationError { compile_error: Some(msg), .. }) => {
// Schema mismatch caught at codegen time (rule/field type mismatch,
// unknown CEL field reference, etc.).
eprintln!("schema error: {msg}");
}
Err(ValidationError { runtime_error: Some(msg), .. }) => {
// Rule precondition failed at runtime (e.g. bytes.pattern on non-UTF-8 input).
eprintln!("runtime error: {msg}");
}
Err(ValidationError { violations, .. }) => {
// One or more field rules failed — the common case.
for v in violations {
eprintln!("[{}] {}", v.rule_id, v.message);
}
}
}Installation
Library — add to Cargo.toml:
protovalidate-buffa = "0.5.0"Codegen plugin — install from source:
cargo install \
--git https://github.com/mathematic-inc/protovalidate-buffa \
--tag protoc-gen-protovalidate-buffa-v0.5.0 \
protoc-gen-protovalidate-buffaThen wire it into buf.gen.yaml as before:
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allv0.4.0
protovalidate-buffa v0.4.0
protovalidate-buffa v0.4.0 lands full compatibility with buffa 0.6 and connectrpc 0.6 — the latest releases of both upstream frameworks — and keeps the 100% conformance record firmly intact. If you've been waiting to upgrade your buffa runtime while staying on validated, type-safe protovalidate rules, this release is your green light.
⚠️ Breaking Changes
This release tracks breaking changes in two upstream crates. Bumping protovalidate-buffa to 0.4.0 implies upgrading your own dependencies to match:
| Dependency | Old | New |
|---|---|---|
buffa |
0.5 | 0.6 |
connectrpc |
0.4 | 0.6 (if using the connect feature) |
Regenerate your validation code after updating (buf generate) — the plugin is also at 0.4.0 and must match the runtime.
Migrating:
# Cargo.toml
[dependencies]
protovalidate-buffa = "0.4.0"
buffa = "0.6"
connectrpc = "0.6" # only if you use the `connect` feature⚡ Improvements
buffa 0.6 support — no more git patches
Previous releases required a [patch.crates-io] block in your workspace Cargo.toml to point at an unreleased buffa revision. That's gone. buffa 0.6 is on crates.io, so the whole stack resolves cleanly from the registry with no patching needed.
connectrpc 0.6 support
The optional connect feature now targets connectrpc 0.6. ValidationError::into_connect_error continues to map field violations to a structured InvalidArgument Connect error — the API is unchanged, only the version floor moves.
// Error handling is the same — match on typed fields, not string prefixes.
match req.validate() {
Ok(()) => { /* proceed */ }
Err(e) if e.compile_error.is_some() => {
// Schema mismatch caught at codegen time — should not reach prod.
tracing::error!(err = %e.compile_error.as_deref().unwrap_or(""), "compile error");
return Err(e.into_connect_error());
}
Err(e) if e.runtime_error.is_some() => {
// Rule precondition failed at runtime (e.g. non-UTF-8 bytes under `pattern`).
return Err(e.into_connect_error());
}
Err(e) => {
// Field violations — the common path.
return Err(e.into_connect_error());
}
}2872 / 2872 conformance — no regressions
The full upstream protovalidate-conformance suite still passes at 2872 / 2872 (100%) across proto2, proto3, and editions 2023. The dependency upgrade did not regress a single test case.
📦 Installation
Library consumers — add to Cargo.toml:
[dependencies]
protovalidate-buffa = "0.4.0"Codegen plugin — install the buf generate plugin:
cargo install \
--git https://github.com/mathematic-inc/protovalidate-buffa \
--tag v0.4.0 \
protoc-gen-protovalidate-buffaThen wire it into buf.gen.yaml:
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allFull changelog: v0.3.0...v0.4.0
protovalidate-buffa-protos-v0.4.0
protovalidate-buffa v0.4.0 brings full compatibility with buffa 0.6 and connectrpc 0.6, keeping the entire validation stack in lockstep with the latest buffa runtime. All four published crates have been updated together so you can upgrade your service in one shot. The library continues to pass 2872 / 2872 (100%) of the upstream protovalidate-conformance suite across proto2, proto3, and editions 2023 — every rule, every edge case, zero regressions. If you're upgrading your buffa or connectrpc dependency, this is your paired protovalidate release.
💥 Breaking Changes
Upgraded to buffa 0.6 + connectrpc 0.6
The runtime (protovalidate-buffa), protos crate (protovalidate-buffa-protos), and codegen plugin (protoc-gen-protovalidate-buffa) all target buffa 0.6 and connectrpc 0.6. If your service is still on buffa 0.5.x, stay on v0.3.0 until you're ready to upgrade.
Update your workspace together:
# Cargo.toml
[dependencies]
buffa = "0.6"
connectrpc = "0.6"
protovalidate-buffa = "0.4.0"✅ Conformance: 2872 / 2872
The full upstream protovalidate-conformance suite passes — all 2872 cases across proto2, proto3, and editions 2023. Every standard rule family is covered: strings, bytes, numerics, booleans, enums, repeated fields, maps, google.protobuf.Any, Duration, Timestamp, FieldMask, wrapper types, predefined rules, CEL expressions, and cross-proto cel_expression references.
Worth calling out: every CEL rule is transpiled to native Rust at codegen time — there is no interpreter at runtime. Generated validate() calls are direct struct field walks that LLVM can inline, with zero per-call Value / HashMap allocations for well-formed input.
Error handling
ValidationError carries three orthogonal, typed signals — match on the fields, not on string prefixes:
use protovalidate_buffa::Validate;
match req.validate() {
Ok(()) => { /* proceed */ }
Err(e) if e.compile_error.is_some() => {
// Schema-level mismatch caught at codegen time
// (rule/field type error, bad oneof spec, CEL referencing unknown field)
eprintln!("compile error: {}", e.compile_error.unwrap());
}
Err(e) if e.runtime_error.is_some() => {
// Rule precondition could not be evaluated at runtime
// (e.g. bytes.pattern on non-UTF-8 input)
eprintln!("runtime error: {}", e.runtime_error.unwrap());
}
Err(e) => {
// Per-field violations — map to ConnectError::invalid_argument
return Err(e.into_connect_error());
}
}Zero-boilerplate validation with #[connect_impl]
Apply #[protovalidate_buffa::connect_impl] to a Connect service impl block and req.validate()? is inserted at the top of every handler automatically — no per-handler discipline required:
#[protovalidate_buffa::connect_impl]
impl UserService for UserServiceImpl {
async fn create_user(
&self,
ctx: Context,
request: OwnedView<pb::CreateUserRequestView<'static>>,
) -> Result<(pb::CreateUserResponse, Context), ConnectError> {
// Every request arriving here is already validated.
}
}Installation
Library — add to Cargo.toml:
[dependencies]
protovalidate-buffa = "0.4.0"Codegen plugin — install once and wire into buf.gen.yaml:
cargo install \
--git https://github.com/mathematic-inc/protovalidate-buffa \
--tag protovalidate-buffa-protos-v0.4.0 \
protoc-gen-protovalidate-buffa# buf.gen.yaml
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allprotoc-gen-protovalidate-buffa-v0.4.0
protoc-gen-protovalidate-buffa v0.4.0
protovalidate-buffa v0.4.0 is a compatibility release that tracks the latest buffa and connectrpc ecosystem — your generated validation code stays in lock-step with the runtime it was built for. All 2872 / 2872 (100%) upstream protovalidate-conformance cases continue to pass across proto2, proto3, and editions 2023, so you're upgrading with zero regressions and full confidence.
⚡ Improvements
Updated to buffa 0.6 and connectrpc 0.6
The entire workspace now targets the latest buffa and connectrpc releases. If your project already uses buffa 0.6 or connectrpc 0.6, this release removes the version conflict and lets you upgrade cleanly.
Affected crates: protovalidate-buffa, protoc-gen-protovalidate-buffa, protovalidate-buffa-protos.
Breaking Changes
buffa0.5 → 0.6. The runtime and plugin both now requirebuffa = "0.6". Update yourCargo.tomland re-runbuf generateto regenerate message types against the new runtime.connectrpc0.4 → 0.6. If you use theconnectfeature (enabled by default), your project must useconnectrpc0.6. TheValidationError::into_connect_erroradapter is fully compatible with the new version.
Updating is a one-line bump in your Cargo.toml:
[dependencies]
protovalidate-buffa = "0.4.0"Error handling is unchanged — keep matching on the typed fields, not on string prefixes:
match req.validate() {
Ok(()) => { /* proceed */ }
Err(e) if e.compile_error.is_some() => {
// Schema-level mismatch caught at codegen time — should never reach prod
panic!("validation schema error: {}", e.compile_error.unwrap());
}
Err(e) if e.runtime_error.is_some() => {
// Rule precondition failed at runtime (e.g. non-UTF-8 input under `pattern`)
return Err(e.into_connect_error());
}
Err(e) => {
// Field violations — the common case
return Err(e.into_connect_error());
}
}Conformance
2872 / 2872 (100%) — every case in the upstream protovalidate-conformance suite passes, covering proto2, proto3, and editions 2023. No regressions from the dependency bump.
Installation
Library (protovalidate-buffa)
Add to your Cargo.toml:
[dependencies]
protovalidate-buffa = "0.4.0"Codegen plugin (protoc-gen-protovalidate-buffa)
cargo install \
--git https://github.com/mathematic-inc/protovalidate-buffa \
--tag protoc-gen-protovalidate-buffa-v0.4.0 \
protoc-gen-protovalidate-buffaThen wire it into your buf.gen.yaml:
plugins:
- local: protoc-gen-protovalidate-buffa
out: gen/protovalidate
strategy: allRe-run buf generate after upgrading the plugin — generated code references buffa 0.6 types.
v0.3.0
protovalidate-buffa v0.3.0
This release lands the biggest architectural leap since the project's debut: every CEL rule in your .proto is now transpiled to native Rust at codegen time, and the runtime interpreter is gone for good. Your validate() calls are plain struct field walks — no Value trees, no HashMap allocations, nothing to allocate or interpret on the hot path. LLVM can inline the whole thing. And the full 2872 / 2872 (100%) upstream protovalidate-conformance suite still passes, covering proto2, proto3, and editions 2023. Zero regressions, dramatically lower runtime overhead.
✨ New Features
Compile-time CEL transpilation
protoc-gen-protovalidate-buffa now ships a full typed AST visitor (emit/cel_compile.rs) that walks every CEL expression in (buf.validate.field).cel and (buf.validate.message).cel rules and emits a native Rust TokenStream. By the time you run buf generate, your CEL is already Rust.
Before v0.3.0: the runtime hosted a CEL interpreter, built a Context per call, allocated a Value tree to represent the message, evaluated the expression, and interpreted the result — all on every call.
After v0.3.0: a custom rule like this:
message TransferRequest {
double amount = 1 [(buf.validate.field).cel = {
id: "positive_amount",
message: "amount must be positive",
expression: "this > 0.0"
}];
}…causes the plugin to emit code equivalent to:
// generated — direct comparison, zero allocations
if !(self.amount > 0.0_f64) {
violations.push(Violation {
field_path: field_path!("amount"),
constraint_id: "positive_amount".into(),
message: "amount must be positive".into(),
});
}The transpiler handles the full breadth of protovalidate CEL:
- Literals, idents, selects —
this.field,rule.gte, selector chains - Comprehensions —
all,exists,map,filter,.map(filter, expr), nested comprehensions - String/bytes/list/map ops —
size(),contains(),startsWith(),endsWith(),indexOf(),substring(),lowerAscii(),upperAscii(), regex match - Semantic validators —
is_ip/is_ip_prefix(including dynamic(ver, strict)dispatch),is_hostname,is_email,is_uri,is_uri_ref has()semantics — correct per field kind: scalar, optional, wrapper, message, repeated- Timestamp/duration — constructors, timezone-aware accessors (
t.getHours("America/New_York")) - Const-folding —
rule.fooreferences in predefined CEL rules inline as Rust literals
Codegen-time detection of always-runtime-error CEL
When the transpiler can prove a CEL expression would unconditionally raise a runtime error — for example, accessing a non-existent field via dyn(this).no_such_field — it now surfaces a FallbackKind::RuntimeError marker at codegen time. Rule definition bugs that previously silently passed validation now fail loudly during code generation, where they belong.
⚡ Improvements
Runtime CEL module: 980 lines → 135 lines
The cel interpreter has been removed entirely from protovalidate-buffa's dependencies. The runtime cel.rs now contains only what generated code actually needs:
CelScalarwidening trait — uniformi64/u64/f64views over scalar fields, includingEnumValue<E>duration_from_secs_nanos/timestamp_from_secs_nanos— WKT constructorsnow_local— current timestamp for time-relative rules
No Value. No Context. No Program. The plugin continues to use the cel crate's parser (parse-only, no evaluator feature) to build the AST, but evaluation never touches your running binary.
86 CEL transpiler unit tests
The conformance suite exercises rules end-to-end but most cases constant-fold to predefined rules — it barely stresses the transpiler in isolation. emit/cel_compile.rs ships 86 targeted unit tests grouped by risk:
| Bucket | What's covered |
|---|---|
| Type system | Cross-type comparison, int/uint/double casts, negation, arithmetic, modulo |
| Comprehensions | all/exists/map/filter, nesting, predicate captures, short-list literals |
has() semantics |
One test per SchemaFieldKind variant, plus unknown-field and dyn paths |
| Sub-message resolution | MessageRef not in index, deep chains, self-referential schemas |
| String semantics | Unicode size() (chars, not bytes), concat, regex, indexOf, substring |
| Map indexing | string/int/bool keys, size(), .all() over keys |
| Const-folding | Int/UInt/Double/Bool/Str RuleConst inlines as a Rust literal |
Three tests document current transpiler gaps — int == uint cross-type equality, [].all(x, …) on empty list literals, and key in mapValue — so they won't sneak through conformance unnoticed.
Dependency cleanup
cargo +nightly udeps caught several transitive deps that were no longer needed after the interpreter was removed:
buffa-typesdropped fromprotovalidate-buffa,protovalidate-buffa-protos, andprotovalidate-buffa-conformanceconnectrpcdropped fromprotovalidate-buffa-macros(the proc-macro emits paths that resolve through the user's own transitive deps)
Lint and doc hygiene
#[allow]→#[expect]where the lint reliably fires; stale#[allow]s removedRUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-depspasses cleanly#[cfg(feature = "connect")]-gates theconnect_implre-export socargo test --no-default-featurescompiles without errors- Inline let-chains replace nested
if letblocks; const-eligible helpers markedconst fn;Defaultderived where manually implemented before
Error handling
Match on the typed fields — never on rule_id string prefixes:
match request.validate() {
Ok(()) => { /* proceed with validated input */ }
Err(e) if e.compile_error.is_some() => {
// Schema/type mismatch detected at codegen time.
// Your .proto annotations don't match the field type —
// fix the .proto, regenerate, and redeploy.
tracing::error!("validation schema error: {}", e.compile_error.as_deref().unwrap_or(""));
return Err(ConnectError::internal("invalid generated validation code"));
}
Err(e) if e.runtime_error.is_some() => {
// A rule precondition couldn't be evaluated at runtime.
// Common cause: bytes.pattern applied to non-UTF-8 input.
tracing::warn!("validation runtime error: {}", e.runtime_error.as_deref().unwrap_or(""));
return Err(ConnectError::invalid_argument("request could not be validated"));
}
Err(e) => {
// The normal path: one or more field rules failed.
return Err(e.into_connect_error()); // requires the `connect` feature
}
}Installation
Library consumers — add to Cargo.toml:
[dependencies]
protovalidate-buffa = "0.3.0"Codegen plugin — install from the repo at the release tag:
cargo install \
--git https://github.com/mathematic-inc/protovalidate-buffa \
--tag v0.3.0 \
protoc-gen-protovalidate-buffa