Web frameworks
A Tower layer that validates JSON request bodies for any http-based service.

ValidateJsonLayer<T> validates JSON request bodies before the inner service runs. It is framework-agnostic: it depends on tower-layer, tower-service, http and http-body, not on a web framework, so it works with an Axum Router, a hyper service (via hyper-util's TowerToHyperService) or a plain service_fn. Error bodies follow the web adapters overview.

Install

Cargo.toml
[dependencies]
vld = "0.5"
vld-tower = "0.5"

No features. MSRV 1.85, see MSRV.

Example

use bytes::Bytes;
use http::{Request, Response, StatusCode};
use http_body_util::Full;
use tower::{ServiceBuilder, ServiceExt};
use vld_tower::{validated, ValidateJsonLayer, ValidatedBody};

vld::schema! {
    #[derive(Debug, Clone)]
    pub struct CreateUser {
        pub name: String  => vld::string().min(2),
        pub email: String => vld::string().email(),
    }
}

async fn handler(
    req: Request<ValidatedBody<Full<Bytes>>>,
) -> Result<Response<Full<Bytes>>, std::convert::Infallible> {
    // None: the request was not JSON and passed the layer unread.
    let name = validated::<CreateUser>(&req).map_or("not JSON".into(), |u| u.name);
    Ok(Response::new(Full::new(Bytes::from(name))))
}

let svc = ServiceBuilder::new()
    .layer(ValidateJsonLayer::<CreateUser>::new())
    .service_fn(handler);

let req = Request::post("/users")
    .header("content-type", "application/json")
    .body(Full::new(Bytes::from(r#"{"name":"A","email":"bad"}"#)))
    .unwrap();
let resp = svc.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY);

With Axum

use axum::http::StatusCode;
use axum::routing::post;
use axum::{Extension, Router};
use vld_tower::{ValidateJsonLayer, Validated};

async fn create(
    user: Option<Extension<Validated<CreateUser>>>,
) -> Result<String, (StatusCode, &'static str)> {
    let Some(Extension(Validated(user))) = user else {
        return Err((StatusCode::UNSUPPORTED_MEDIA_TYPE, "expected JSON"));
    };
    Ok(format!("created {}", user.name))
}

let app = Router::new()
    .route("/users", post(create))
    .layer(ValidateJsonLayer::<CreateUser>::new());

For Axum handlers, vld-axum extractors are usually simpler; the layer fits when validation must happen before routing or in non-Axum stacks.

API

ItemPurpose
ValidateJsonLayer<T>The layer; new() (2 MiB limit) or with_limit(bytes)
ValidateJsonService<S, T>The service it produces
Validated<T>Extension wrapper holding the validated value (derefs to T)
validated::<T>(&req)Option<T> from the request extensions
ValidatedBody<B>http_body_util::Either<Full<Bytes>, B>, the request and response body type
preludeAll of the above plus vld::prelude::*

Status codes

SituationStatus
Validation failed, malformed JSON, body read error422
Body over the limit413
Not JSON (Content-Type is not application/json / application/*+json)passed through, no validation

Gotchas

  • validated() returns Option<T>.
  • The inner service receives Request<ValidatedBody<B>>: Left holds the buffered bytes of a validated JSON request, Right the untouched body of anything else. Responses are Response<ValidatedBody<ResBody>>.
  • The layer never returns an error of its own; its error type is the inner service's.
  • T must be Clone + Send + Sync + 'static.