rust 74 lines · 8 steps

A validated JSON extractor in Axum

Wrap Axum's Json extractor so request bodies are both deserialized and validated before a handler ever runs.

Explained by highlit
1use axum::{
2 async_trait,
3 extract::{rejection::JsonRejection, FromRequest, Request},
4 http::StatusCode,
5 response::{IntoResponse, Response},
6 Json,
7};
8use serde::de::DeserializeOwned;
9use serde_json::json;
10use std::collections::BTreeMap;
11use validator::{Validate, ValidationErrors};
12 
13pub struct ValidatedJson<T>(pub T);
14 
15pub enum ValidationRejection {
16 Json(JsonRejection),
17 Invalid(ValidationErrors),
18}
19 
20#[async_trait]
21impl<T, S> FromRequest<S> for ValidatedJson<T>
22where
23 T: DeserializeOwned + Validate,
24 S: Send + Sync,
25{
26 type Rejection = ValidationRejection;
27 
28 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
29 let Json(value) = Json::<T>::from_request(req, state)
30 .await
31 .map_err(ValidationRejection::Json)?;
32 value.validate().map_err(ValidationRejection::Invalid)?;
33 Ok(ValidatedJson(value))
34 }
35}
36 
37impl IntoResponse for ValidationRejection {
38 fn into_response(self) -> Response {
39 match self {
40 ValidationRejection::Json(rejection) => (
41 rejection.status(),
42 Json(json!({ "message": rejection.body_text() })),
43 )
44 .into_response(),
45 ValidationRejection::Invalid(errors) => {
46 let fields: BTreeMap<_, Vec<String>> = errors
47 .field_errors()
48 .into_iter()
49 .map(|(field, errs)| {
50 let messages = errs
51 .iter()
52 .map(|e| {
53 e.message
54 .as_ref()
55 .map(|m| m.to_string())
56 .unwrap_or_else(|| e.code.to_string())
57 })
58 .collect();
59 (field, messages)
60 })
61 .collect();
62 
63 (
64 StatusCode::UNPROCESSABLE_ENTITY,
65 Json(json!({
66 "message": "Validation failed",
67 "errors": fields,
68 })),
69 )
70 .into_response()
71 }
72 }
73 }
74}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Custom extractors let you enforce invariants once, so handlers receive data that is already guaranteed valid.
  2. 2Splitting rejection into distinct variants keeps deserialization and validation errors separately shaped in the response.
  3. 3Implementing IntoResponse on your error type gives you full control over status codes and JSON error bodies.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A validated JSON extractor in Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code