What is the purpose of axum::Json extractor's Json wrapper for request/response body handling?
axum::Json is a typed wrapper that handles automatic serialization and deserialization of JSON request and response bodies using serde, integrating with axum's extractor system for requests and response conversion for replies. It provides a type-safe interface to JSON data, automatically parsing incoming JSON into typed structs and serializing outgoing structs into JSON responses with the correct Content-Type header.
The Role of Json as a Wrapper
use axum::Json;
use serde::{Deserialize, Serialize};
// Json<T> is a simple wrapper around T that enables JSON handling
#[derive(Deserialize)]
struct CreateUser {
username: String,
email: String,
}
#[derive(Serialize)]
struct User {
id: u64,
username: String,
email: String,
}
// Json<CreateUser> wraps the struct for extraction
// Json<User> wraps the struct for responseJson<T> wraps any serializable/deserializable type to enable automatic JSON conversion.
Json as an Extractor for Requests
use axum::{
Json,
extract::FromRequest,
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct LoginRequest {
username: String,
password: String,
}
// Json implements FromRequest, so it can be used as an extractor
async fn login(
Json(request): Json<LoginRequest>,
) -> impl IntoResponse {
// request is already deserialized from JSON body
// No manual parsing needed
format!("Logging in as {}", request.username)
}
// The extractor:
// 1. Reads the request body
// 2. Parses it as JSON
// 3. Deserializes into LoginRequest
// 4. Returns the wrapped valueJson implements FromRequest, automatically deserializing JSON bodies into typed structs.
Json as a Response for Replies
use axum::{Json, response::IntoResponse, http::StatusCode};
use serde::Serialize;
#[derive(Serialize)]
struct UserResponse {
id: u64,
name: String,
}
async fn get_user(user_id: u64) -> impl IntoResponse {
let user = UserResponse {
id: user_id,
name: "Alice".to_string(),
};
// Json wrapper handles:
// 1. Serialization to JSON
// 2. Setting Content-Type: application/json
// 3. Setting appropriate status code
Json(user)
}
// Response includes:
// - Status: 200 OK
// - Content-Type: application/json
// - Body: {"id":1,"name":"Alice"}Json implements IntoResponse, automatically serializing structs to JSON with proper headers.
Complete Request-Response Cycle
use axum::{Json, http::StatusCode, response::IntoResponse};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreatePost {
title: String,
content: String,
}
#[derive(Serialize)]
struct Post {
id: u64,
title: String,
content: String,
}
// Full cycle: receive JSON, return JSON
async fn create_post(
Json(input): Json<CreatePost>,
) -> impl IntoResponse {
// Input: JSON body automatically deserialized
let post = Post {
id: 1,
title: input.title,
content: input.content,
};
// Output: struct automatically serialized to JSON
(StatusCode::CREATED, Json(post))
}
// Request:
// POST /posts
// Content-Type: application/json
// {"title":"Hello","content":"World"}
//
// Response:
// HTTP/1.1 201 Created
// Content-Type: application/json
// {"id":1,"title":"Hello","content":"World"}The complete flow: Json extracts request bodies and serializes response bodies.
Error Handling for Malformed JSON
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Deserialize;
#[derive(Deserialize)]
struct Item {
name: String,
}
// When JSON is malformed, Json extractor returns an error
// axum automatically converts this to a 400 Bad Request
async fn create_item(Json(item): Json<Item>) -> impl IntoResponse {
// If request body is:
// - Not valid JSON β 400 Bad Request
// - JSON doesn't match Item struct β 400 Bad Request
// - Content-Type not application/json β 400 Bad Request
(StatusCode::OK, Json(item))
}
// Custom error handling for JSON errors
async fn create_item_custom(
result: Result<Json<Item>, axum::extract::rejection::JsonRejection>,
) -> impl IntoResponse {
match result {
Ok(Json(item)) => (StatusCode::OK, Json(item)),
Err(rejection) => {
// Handle specific JSON errors
let message = match rejection {
axum::extract::rejection::JsonRejection::JsonDataError(_) => {
"Invalid JSON data"
}
axum::extract::rejection::JsonRejection::JsonSyntaxError(_) => {
"Invalid JSON syntax"
}
axum::extract::rejection::JsonRejection::MissingJsonContentType(_) => {
"Missing Content-Type: application/json"
}
_ => "Unknown JSON error",
};
(StatusCode::BAD_REQUEST, message)
}
}
}Json extractor automatically rejects malformed JSON with 400 Bad Request.
Content-Type Requirements
use axum::Json;
use serde::Deserialize;
#[derive(Deserialize)]
struct Data {
value: i32,
}
// Json extractor requires Content-Type: application/json
// β
This request works:
// POST /endpoint
// Content-Type: application/json
// {"value":42}
// β This request fails with JsonRejection::MissingJsonContentType:
// POST /endpoint
// {"value":42}
// β This request fails with JsonRejection::JsonSyntaxError:
// POST /endpoint
// Content-Type: application/json
// {invalid json}
// β This request fails with JsonRejection::JsonDataError:
// POST /endpoint
// Content-Type: application/json
// {"wrong_field":42}Json requires the Content-Type: application/json header for requests.
Combining Json with Other Extractors
use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct UpdateUser {
name: Option<String>,
email: Option<String>,
}
#[derive(Deserialize)]
struct UserQuery {
notify: bool,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
email: String,
}
#[derive(Clone)]
struct AppState {
// Application state
}
// Json combined with other extractors
async fn update_user(
Path(user_id): Path<u64>, // URL path parameter
Query(query): Query<UserQuery>, // Query parameters
State(state): State<AppState>, // Application state
Json(updates): Json<UpdateUser>, // JSON body
) -> impl IntoResponse {
// All extractors work together:
// - user_id extracted from /users/:user_id
// - query extracted from ?notify=true
// - state injected from app
// - updates deserialized from JSON body
let user = User {
id: user_id,
name: updates.name.unwrap_or_default(),
email: updates.email.unwrap_or_default(),
};
(StatusCode::OK, Json(user))
}Json combines with other extractors; axum handles extraction ordering.
Why Json is a Wrapper, Not a Trait
use axum::Json;
use serde::{Deserialize, Serialize};
// Json is a struct wrapper: pub struct Json<T>(pub T);
#[derive(Deserialize)]
struct Input {
value: i32,
}
#[derive(Serialize)]
struct Output {
result: i32,
}
// The wrapper pattern enables:
// 1. Tuple destructuring in handler signatures
// 2. Clear separation between the data and its JSON representation
// 3. Automatic Content-Type header setting
async fn process(Json(input): Json<Input>) -> Json<Output> {
// Destructuring extracts the inner value
// input: Input (not Json<Input>)
// Return type specifies Json wrapper
Json(Output { result: input.value * 2 })
}
// Alternative: return impl IntoResponse
async fn process_alt(Json(input): Json<Input>) -> impl IntoResponse {
// Same result, more explicit about response
Json(Output { result: input.value * 2 })
}Json<T> is a struct wrapper (pub struct Json<T>(pub T)) enabling tuple destructuring and type-level JSON representation.
Json vs Manual JSON Handling
use axum::{
Json,
body::Body,
http::{StatusCode, header},
response::IntoResponse,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
struct Item {
id: u64,
name: String,
}
// Using Json wrapper (recommended)
async fn with_json(Json(item): Json<Item>) -> impl IntoResponse {
(StatusCode::OK, Json(item))
}
// Manual handling (not recommended)
async fn manual_json(body: Body) -> impl IntoResponse {
// 1. Collect body bytes
let bytes = axum::body::to_bytes(body, 1024 * 1024).await?;
// 2. Parse JSON
let item: Item = serde_json::from_slice(&bytes)?;
// 3. Serialize response
let json = serde_json::to_string(&item)?;
// 4. Build response with headers
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
json,
)
}
// Json wrapper handles all of this automatically:
// - Body collection
// - JSON parsing
// - Error handling
// - Content-Type headers
// - SerializationJson eliminates boilerplate for body collection, parsing, headers, and serialization.
Response Status Codes
use axum::{Json, http::StatusCode, response::IntoResponse};
use serde::Serialize;
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
// Default: 200 OK
async fn get_user() -> Json<User> {
Json(User { id: 1, name: "Alice".into() })
// Response: 200 OK, Content-Type: application/json
}
// Custom status code
async fn create_user() -> impl IntoResponse {
let user = User { id: 1, name: "Alice".into() };
// Tuple syntax: (status, Json body)
(StatusCode::CREATED, Json(user))
// Response: 201 Created, Content-Type: application/json
}
// Error responses
async fn get_user_or_404(id: u64) -> impl IntoResponse {
if id == 0 {
return (StatusCode::NOT_FOUND, Json("User not found"));
}
(StatusCode::OK, Json(User { id, name: "Alice".into() }))
}
// Multiple response types
#[derive(Serialize)]
struct ErrorResponse {
error: String,
}
async fn layered_response(success: bool) -> impl IntoResponse {
if success {
(StatusCode::OK, Json(User { id: 1, name: "Alice".into() }))
} else {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse { error: "Invalid request".into() }),
)
}
}Json defaults to 200 OK; combine with status codes using tuples for custom responses.
Json with Axum Router
use axum::{
Json,
Router,
routing::{get, post},
http::StatusCode,
response::IntoResponse,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateRequest {
name: String,
}
#[derive(Serialize)]
struct CreateResponse {
id: u64,
name: String,
}
async fn create_item(Json(req): Json<CreateRequest>) -> impl IntoResponse {
let response = CreateResponse {
id: 1,
name: req.name,
};
(StatusCode::CREATED, Json(response))
}
async fn get_item() -> impl IntoResponse {
Json(CreateResponse { id: 1, name: "Item".into() })
}
fn create_router() -> Router {
Router::new()
.route("/items", post(create_item)) // Expects JSON body
.route("/items/:id", get(get_item)) // Returns JSON
}
// All routes use Json for request/response handlingJson integrates seamlessly with axum's routing system.
Deserialization Configuration
use axum::Json;
use serde::Deserialize;
// The Json extractor uses serde's default settings
// Customize deserialization with serde attributes
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
struct User {
#[serde(rename = "user_name")]
name: String,
#[serde(default)]
active: bool, // Defaults to false if missing
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
}
async fn create_user(Json(user): Json<User>) -> impl IntoResponse {
// Accepts: {"user_name": "Alice"}
// name: "Alice", active: false, email: None
// Accepts: {"user_name": "Bob", "active": true}
// name: "Bob", active: true, email: None
Json(user)
}Json uses standard serde attributes for customizing serialization and deserialization.
Complete Summary
use axum::{Json, http::StatusCode, response::IntoResponse};
use serde::{Deserialize, Serialize};
fn complete_summary() {
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// β Aspect β Json<T> as Extractor β Json<T> as Response β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
// β Direction β Request (incoming) β Response (outgoing) β
// β Implementation β FromRequest β IntoResponse β
// β Operation β Deserialize JSON β T β Serialize T β JSON β
// β Headers β Requires Content-Type β Sets Content-Type β
// β Errors β JsonRejection β Serialize errors β
// β Usage pattern β Json<T> in params β Json(value) in response β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Key behaviors:
// As extractor (request):
// 1. Reads request body
// 2. Validates Content-Type: application/json
// 3. Parses JSON
// 4. Deserializes into T
// 5. Returns JsonRejection on any error
// 6. Unwraps via tuple destructuring: Json(data)
// As response (reply):
// 1. Serializes T to JSON
// 2. Sets Content-Type: application/json
// 3. Returns 200 OK by default
// 4. Customize status: (StatusCode::CREATED, Json(data))
}
// Key insight:
// axum::Json<T> is a typed wrapper that integrates with axum's extractor
// and response systems. As an extractor, it automatically deserializes
// JSON request bodies into typed structs, handling Content-Type
// validation and parsing errors. As a response, it serializes structs
// to JSON with the correct Content-Type header.
//
// The wrapper pattern enables:
// - Tuple destructuring in handler parameters
// - Type-safe JSON handling throughout
// - Automatic error responses for malformed JSON
// - Clean separation between data and its JSON representation
//
// Json<T> works with any T that implements Serialize (for responses)
// and Deserialize (for requests), making it a universal JSON handler
// for axum applications.Key insight: axum::Json<T> is a wrapper type that serves dual purposesβas an extractor, it implements FromRequest to automatically deserialize JSON request bodies into T, and as a response, it implements IntoResponse to serialize T into JSON with the Content-Type: application/json header. The wrapper pattern enables clean tuple destructuring (Json(data): Json<T>) in handler signatures, type-safe JSON handling, automatic 400 Bad Request responses for malformed JSON, and eliminates boilerplate for body collection, parsing, headers, and serialization. Use (StatusCode, Json(T)) tuples for custom status codes beyond the default 200 OK.
