Web frameworks
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
| Item | Purpose |
|---|---|
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 |
prelude | All of the above plus vld::prelude::* |
Status codes
| Situation | Status |
|---|---|
| Validation failed, malformed JSON, body read error | 422 |
| Body over the limit | 413 |
Not JSON (Content-Type is not application/json / application/*+json) | passed through, no validation |
Gotchas
validated()returnsOption<T>.- The inner service receives
Request<ValidatedBody<B>>:Leftholds the buffered bytes of a validated JSON request,Rightthe untouched body of anything else. Responses areResponse<ValidatedBody<ResBody>>. - The layer never returns an error of its own; its error type is the inner service's.
Tmust beClone + Send + Sync + 'static.