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.