Validation helpers for tonic gRPC services: attach rules to prost messages, validate request metadata, and turn failures into Status::invalid_argument. Only tonic::{Request, Status} are used, so no tonic transport features are needed.
Install
[dependencies]
vld = "0.5"
vld-tonic = "0.5"
The dependency is tonic >=0.12, <0.15; there are no version features. MSRV 1.85, see MSRV.
Example
use tonic::{Request, Response, Status};
// Generated by prost; vld needs no extra derives.
#[derive(Clone, PartialEq, prost::Message)]
pub struct CreateUserRequest {
#[prost(string, tag = "1")]
pub name: String,
#[prost(string, tag = "2")]
pub email: String,
}
vld_tonic::impl_validate!(CreateUserRequest {
name => vld::string().min(2).max(100),
email => vld::string().email(),
});
vld::schema! {
#[derive(Debug, Clone)]
pub struct AuthMeta {
pub authorization: String => vld::string().starts_with("Bearer "),
}
}
async fn create_user(request: Request<CreateUserRequest>) -> Result<Response<()>, Status> {
let auth: AuthMeta = vld_tonic::validate_metadata(&request)?;
let msg = vld_tonic::validate(request)?; // Status::invalid_argument on failure
println!("{} by {}", msg.name, auth.authorization);
Ok(Response::new(()))
}
impl_validate! builds on vld::impl_rules!: it adds inherent validate() / is_valid() methods and implements VldMessage. Each listed field is serialized with serde and checked by its schema.
Validate against a separate schema
When a vld::schema! struct mirrors the message, validate a serde::Serialize message against it:
let msg = vld_tonic::validate_with::<UserSchema, _>(request)?;
Metadata interceptor
Server::builder()
.add_service(MyServiceServer::with_interceptor(
my_service,
vld_tonic::metadata_interceptor::<AuthMeta>,
))
.serve(addr)
.await?;
// In the handler:
let auth = vld_tonic::validated_metadata::<AuthMeta, _>(&request)
.ok_or_else(|| Status::unauthenticated("missing auth"))?;
API
| Item | Purpose |
|---|---|
impl_validate!(Type { field => schema, .. }) | Attach rules to a message, implement VldMessage |
VldMessage | Trait with vld_validate(&self) -> Result<(), VldError> |
validate(request) / validate_ref(&msg) | Validate a VldMessage |
validate_with::<S, _>(request) / validate_with_ref::<S, _>(&msg) | Validate a Serialize message against schema S |
validate_metadata::<T, _>(&request) | Parse metadata into T |
metadata_interceptor::<T> | Interceptor that validates metadata and stores Validated<T> |
validated_metadata::<T, _>(&request) | Option<T> stored by the interceptor |
Validated<T> | Extension wrapper (derefs to T) |
vld_status(&err) / vld_status_with_code(&err, code) | VldError to Status |
prelude | All of the above plus vld::prelude::* |
Status format
vld_status returns Code::InvalidArgument with a JSON message:
{
"error": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{ "path": ".name", "message": "String must be at least 2 characters", "code": "too_small" }
]
}
path uses the same format as the HTTP adapters (.user.items[0].name); code is IssueCode::key().
Gotchas
- Metadata keys follow the header rules:
x-request-idbecomesx_request_id, keys that already contain_are ignored, binary (-bin) keys are skipped. Values are coerced ("42"is a number, but a string field still accepts it). validate_withserializes the message first; a serialization failure isStatus::internal.