Web frameworks
Validate tonic gRPC messages and metadata with vld schemas.

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

Cargo.toml
[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

ItemPurpose
impl_validate!(Type { field => schema, .. })Attach rules to a message, implement VldMessage
VldMessageTrait 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
preludeAll 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-id becomes x_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_with serializes the message first; a serialization failure is Status::internal.