Skip to main content
The Json extractor and response type makes it easy to work with JSON data in Axum applications.

Extracting JSON request bodies

Use the Json extractor to deserialize JSON from request bodies:

Basic JSON extraction

1

Define your data structure

Create a struct that implements Deserialize:
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUser {
    username: String,
    email: String,
    age: u32,
}
2

Use the Json extractor

Extract JSON in your handler:
use axum::Json;

async fn create_user(Json(payload): Json<CreateUser>) {
    // payload is a CreateUser struct
    println!("Creating user: {}", payload.username);
}
3

Set up the route

use axum::{routing::post, Router};

let app = Router::new()
    .route("/users", post(create_user));
The Json extractor requires the request to have a Content-Type: application/json header. Requests without this header will be rejected with a 415 Unsupported Media Type error.

JSON responses

Return Json to automatically serialize responses:
use axum::Json;
use serde::Serialize;
use uuid::Uuid;

#[derive(Serialize)]
struct User {
    id: Uuid,
    username: String,
    email: String,
}

async fn get_user() -> Json<User> {
    let user = User {
        id: Uuid::new_v4(),
        username: "alice".to_string(),
        email: "alice@example.com".to_string(),
    };
    
    Json(user)
}
This automatically sets the Content-Type: application/json header and serializes the struct.

Working with generic JSON

Use serde_json::Value for dynamic JSON:
use serde_json::Value;

async fn handle_json(Json(payload): Json<Value>) -> String {
    // payload can be any valid JSON
    format!("Received: {}", payload)
}

Complete CRUD example

Here’s a full REST API example:
use axum::{
    extract::Path,
    http::StatusCode,
    response::IntoResponse,
    routing::{get, post, put, delete},
    Json, Router,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Serialize, Deserialize, Clone)]
struct User {
    id: Uuid,
    username: String,
    email: String,
}

#[derive(Deserialize)]
struct CreateUser {
    username: String,
    email: String,
}

#[derive(Deserialize)]
struct UpdateUser {
    username: Option<String>,
    email: Option<String>,
}

// Create
async fn create_user(Json(payload): Json<CreateUser>) -> (StatusCode, Json<User>) {
    let user = User {
        id: Uuid::new_v4(),
        username: payload.username,
        email: payload.email,
    };
    
    (StatusCode::CREATED, Json(user))
}

// Read one
async fn get_user(Path(user_id): Path<Uuid>) -> Json<User> {
    // In a real app, fetch from database
    Json(User {
        id: user_id,
        username: "alice".to_string(),
        email: "alice@example.com".to_string(),
    })
}

// Read all
async fn list_users() -> Json<Vec<User>> {
    // In a real app, fetch from database
    Json(vec![
        User {
            id: Uuid::new_v4(),
            username: "alice".to_string(),
            email: "alice@example.com".to_string(),
        },
    ])
}

// Update
async fn update_user(
    Path(user_id): Path<Uuid>,
    Json(payload): Json<UpdateUser>,
) -> Json<User> {
    // In a real app, update in database
    Json(User {
        id: user_id,
        username: payload.username.unwrap_or_else(|| "alice".to_string()),
        email: payload.email.unwrap_or_else(|| "alice@example.com".to_string()),
    })
}

// Delete
async fn delete_user(Path(user_id): Path<Uuid>) -> StatusCode {
    // In a real app, delete from database
    StatusCode::NO_CONTENT
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users", get(list_users).post(create_user))
        .route("/users/:id", get(get_user).put(update_user).delete(delete_user));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    axum::serve(listener, app).await;
}

Error handling

Handle JSON deserialization errors:
use axum::{
    extract::rejection::JsonRejection,
    http::StatusCode,
    response::{IntoResponse, Response},
};

#[derive(Deserialize)]
struct Input {
    value: i32,
}

async fn handler(
    payload: Result<Json<Input>, JsonRejection>,
) -> Response {
    match payload {
        Ok(Json(input)) => {
            format!("Received: {}", input.value).into_response()
        }
        Err(err) => {
            // Customize error response
            (
                StatusCode::BAD_REQUEST,
                format!("Invalid JSON: {err}"),
            ).into_response()
        }
    }
}

JSON error types

The Json extractor can fail with these errors:
  • MissingJsonContentType: Request missing Content-Type: application/json header (HTTP 415)
  • JsonDataError: JSON is valid but doesn’t match the target type (HTTP 422)
  • JsonSyntaxError: Invalid JSON syntax (HTTP 400)
  • BytesRejection: Failed to buffer request body

Content type variants

The Json extractor accepts various JSON content types:
// All these content types are accepted:
// - application/json
// - application/json; charset=utf-8
// - application/cloudevents+json  (any +json suffix)

async fn flexible_json(Json(data): Json<Value>) -> String {
    format!("Received: {data}")
}

Response customization

Customize JSON responses with status codes and headers:
use axum::http::StatusCode;

async fn create_item(Json(item): Json<NewItem>) -> (StatusCode, Json<Item>) {
    let created_item = save_item(item).await;
    (StatusCode::CREATED, Json(created_item))
}

Performance optimization

For large JSON payloads, consider streaming:
use axum::{
    body::Bytes,
    extract::Request,
    http::StatusCode,
};

async fn large_json_handler(request: Request) -> Result<String, StatusCode> {
    let bytes = Bytes::from_request(request, &())
        .await
        .map_err(|_| StatusCode::BAD_REQUEST)?;
    
    // Deserialize from bytes directly
    let data: MyLargeStruct = Json::<MyLargeStruct>::from_bytes(&bytes)
        .map_err(|_| StatusCode::UNPROCESSABLE_ENTITY)?
        .0;
    
    Ok(format!("Processed {} items", data.items.len()))
}

Testing JSON handlers

#[cfg(test)]
mod tests {
    use super::*;
    use axum::{
        body::Body,
        http::{self, Request, StatusCode},
    };
    use http_body_util::BodyExt;
    use serde_json::json;
    use tower::ServiceExt;

    #[tokio::test]
    async fn test_create_user() {
        let app = Router::new().route("/users", post(create_user));

        let response = app
            .oneshot(
                Request::post("/users")
                    .header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
                    .body(Body::from(
                        serde_json::to_vec(&json!({
                            "username": "alice",
                            "email": "alice@example.com"
                        }))
                        .unwrap(),
                    ))
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::CREATED);

        let body = response.into_body().collect().await.unwrap().to_bytes();
        let user: User = serde_json::from_slice(&body).unwrap();
        assert_eq!(user.username, "alice");
    }

    #[tokio::test]
    async fn test_invalid_json() {
        let app = Router::new().route("/users", post(create_user));

        let response = app
            .oneshot(
                Request::post("/users")
                    .header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
                    .body(Body::from("{invalid json}"))
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    }
}

Best practices

The Json extractor must be the last extractor in a handler if you have multiple extractors, since it consumes the request body.
Always use strongly-typed structs instead of Value when possible. This provides better type safety and clearer API documentation.
Use #[serde(rename_all = "camelCase")] on your structs to automatically convert between Rust’s snake_case and JSON’s camelCase conventions.