Documentation Index Fetch the complete documentation index at: https://mintlify.com/tokio-rs/axum/llms.txt
Use this file to discover all available pages before exploring further.
The Json extractor and response type makes it easy to work with JSON data in Axum applications.
Use the Json extractor to deserialize JSON from request bodies:
Define your data structure
Create a struct that implements Deserialize: use serde :: Deserialize ;
#[derive( Deserialize )]
struct CreateUser {
username : String ,
email : String ,
age : u32 ,
}
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);
}
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:
Accepting any JSON
Building JSON responses
Manipulating 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:
With status code
With headers
Full response control
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 ))
}
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.