rust
57 lines · 7 steps
A validating JSON extractor in Axum
Wrap Axum's Json extractor so request bodies are both deserialized and validated before your handler 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 validator::Validate;
11
12pub struct ValidatedJson<T>(pub T);
13
14#[async_trait]
15impl<T, S> FromRequest<S> for ValidatedJson<T>
16where
17 T: DeserializeOwned + Validate,
18 S: Send + Sync,
19{
20 type Rejection = ValidationRejection;
21
22 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
23 let Json(value) = Json::<T>::from_request(req, state).await?;
24 value.validate()?;
25 Ok(ValidatedJson(value))
26 }
27}
28
29pub enum ValidationRejection {
30 Json(JsonRejection),
31 Invalid(validator::ValidationErrors),
32}
33
34impl From<JsonRejection> for ValidationRejection {
35 fn from(rejection: JsonRejection) -> Self {
36 Self::Json(rejection)
37 }
38}
39
40impl From<validator::ValidationErrors> for ValidationRejection {
41 fn from(errors: validator::ValidationErrors) -> Self {
42 Self::Invalid(errors)
43 }
44}
45
46impl IntoResponse for ValidationRejection {
47 fn into_response(self) -> Response {
48 match self {
49 Self::Json(rejection) => rejection.into_response(),
50 Self::Invalid(errors) => (
51 StatusCode::UNPROCESSABLE_ENTITY,
52 Json(json!({ "message": "validation failed", "errors": errors })),
53 )
54 .into_response(),
55 }
56 }
57}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Implementing FromRequest lets you build custom extractors that run logic before a handler ever executes.
- 2Composing an existing extractor inside your own keeps deserialization concerns separate from validation.
- 3A dedicated rejection type with IntoResponse gives each failure mode its own tailored HTTP response.
Related explainers
ruby
class Order class InvalidTransition < StandardError; end TRANSITIONS = {
A state machine for order transitions in Ruby
state-machine
data-driven
error-handling
Intermediate
8 steps
rust
use std::cmp::Ordering; pub struct PrefixIndex { entries: Vec<String>,
Prefix search with binary partitioning in Rust
binary-search
sorting
case-insensitive
Intermediate
7 steps
php
<?php namespace App\Http\Controllers;
Streaming a filtered CSV export in Laravel
streaming
csv-export
query-builder
Intermediate
9 steps
rust
use std::cmp::Ordering; use std::str::FromStr; #[derive(Debug, Clone, PartialEq, Eq)]
Parsing and ordering semantic versions in Rust
parsing
trait-implementation
ordering
Intermediate
8 steps
javascript
'use client'; import { useEffect } from 'react'; import * as Sentry from '@sentry/nextjs';
How a Next.js error boundary recovers
error-boundary
error-handling
observability
Intermediate
8 steps
ruby
class Registration < ApplicationRecord belongs_to :event validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
Validating registrations in Rails
validations
i18n
error-handling
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-validating-json-extractor-in-axum-explained-rust-98d2/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.