Loading pageā¦
Rust walkthroughs
Loading pageā¦
axum::Json and manually extracting the body as Bytes?axum::Json<T> is a typed extractor that deserializes the request body into a struct using serde, while Bytes extracts the raw body content without any parsing. Json provides type safety and automatic deserialization with proper error handling, but consumes the body and requires valid JSON. Bytes gives you unprocessed access to the raw request body, useful for logging, debugging, or when you need custom parsing logic. The choice depends on whether you need structured data or raw bytes.
use axum::{
Json,
extract::Json as JsonExtractor,
routing::post,
Router,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct CreateUser {
username: String,
email: String,
}
// Json extractor deserializes automatically
async fn create_user(Json(user): Json<CreateUser>) -> String {
format!("Created user: {} with email: {}", user.username, user.email)
}
// The handler receives a fully typed struct
async fn create_user_explicit(JsonExtractor(user): JsonExtractor<CreateUser>) -> String {
// user is CreateUser, not raw bytes
format!("Created user: {} with email: {}", user.username, user.email)
}Json extracts and deserializes in one step, giving you a typed struct.
use axum::{
body::Bytes,
routing::post,
Router,
};
async fn handle_raw_body(body: Bytes) -> String {
// body is raw bytes - no parsing done
format!("Received {} bytes", body.len())
}
async fn inspect_body(body: Bytes) -> String {
// Can inspect raw content
let content = String::from_utf8_lossy(&body);
format!("Raw content: {}", content)
}Bytes gives you the raw request body without any interpretation.
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Deserialize;
#[derive(Deserialize)]
struct User {
username: String,
age: u32,
}
// Json extractor handles errors automatically
async fn create_user(Json(user): Json<User>) -> impl IntoResponse {
// If body is not valid JSON or doesn't match User struct,
// axum returns 400 Bad Request before this handler runs
(StatusCode::OK, format!("User: {}", user.username))
}
// Invalid JSON input:
// {"username": "alice", "age": "not_a_number"}
// Response: 400 Bad Request with JSON error details
// Missing field:
// {"username": "alice"}
// Response: 400 Bad Request (missing "age")
// Valid input:
// {"username": "alice", "age": 30}
// Handler runs with User { username: "alice", age: 30 }Json automatically validates and returns appropriate error responses.
use axum::{
body::Bytes,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Deserialize;
use serde_json;
#[derive(Deserialize)]
struct User {
username: String,
age: u32,
}
async fn create_user_manual(body: Bytes) -> Result<impl IntoResponse, impl IntoResponse> {
// Parse JSON manually from bytes
let user: User = match serde_json::from_slice(&body) {
Ok(user) => user,
Err(e) => {
return Err((
StatusCode::BAD_REQUEST,
format!("Invalid JSON: {}", e),
));
}
};
Ok((StatusCode::OK, format!("User: {}", user.username)))
}With Bytes, you handle parsing and error responses yourself.
use axum::{
body::Bytes,
http::{Request, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Response},
};
use serde::Deserialize;
#[derive(Deserialize)]
struct WebhookPayload {
event_type: String,
data: serde_json::Value,
}
async fn webhook_handler(body: Bytes) -> impl IntoResponse {
// Log raw body for debugging
tracing::info!("Raw webhook body: {}", String::from_utf8_lossy(&body));
// Parse manually
let payload: WebhookPayload = match serde_json::from_slice(&body) {
Ok(p) => p,
Err(e) => {
return (
StatusCode::BAD_REQUEST,
format!("Invalid webhook payload: {}", e),
);
}
};
(StatusCode::OK, format!("Processed: {}", payload.event_type))
}Use Bytes when you need to inspect or log the raw body before parsing.
use axum::body::Bytes;
async fn handle_multiple_formats(body: Bytes) -> impl IntoResponse {
// Check content type manually
let content = String::from_utf8_lossy(&body);
// Parse based on content
if content.starts_with('{') {
// Parse as JSON
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
format!("JSON: {:?}", json)
} else if content.starts_with('<') {
// Parse as XML
format!("XML content")
} else {
// Parse as form data
format!("Form data: {}", content)
}
}Bytes allows format detection and custom parsing logic.
use axum::{
Json,
http::{header, Request, StatusCode},
body::Bytes,
response::IntoResponse,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct Data {
value: String,
}
// Json extractor requires Content-Type: application/json
async fn json_endpoint(Json(data): Json<Data>) -> impl IntoResponse {
format!("Received: {}", data.value)
}
// Request without correct Content-Type fails:
// Content-Type: text/plain
// Body: {"value": "test"}
// Response: 400 Bad Request (Invalid Content-Type)
// Bytes doesn't care about Content-Type
async fn bytes_endpoint(body: Bytes) -> impl IntoResponse {
format!("Received {} bytes", body.len())
}Json enforces Content-Type requirements; Bytes does not.
use axum::{body::Bytes, Json};
use serde::Deserialize;
#[derive(Deserialize)]
struct LargeData {
items: Vec<String>,
metadata: std::collections::HashMap<String, String>,
}
// Json: parses immediately, allocates structures
async fn json_endpoint(Json(data): Json<LargeData>) -> impl IntoResponse {
// data is already parsed and allocated
// O(n) memory for items vector and hashmap
format!("{} items", data.items.len())
}
// Bytes: zero parsing overhead
async fn bytes_endpoint(body: Bytes) -> impl IntoResponse {
// No parsing done, just raw bytes
// If you don't need the data, this is faster
format!("{} bytes received", body.len())
}
// Bytes with deferred parsing
async fn conditional_parse(body: Bytes) -> impl IntoResponse {
// Only parse if needed
if body.len() > 1024 {
return "Body too large, rejected without parsing";
}
// Parse only small bodies
let data: LargeData = serde_json::from_slice(&body).unwrap();
format!("{} items", data.items.len())
}Bytes has lower overhead when you don't need structured data.
use axum::{
body::Bytes,
Json,
http::Request,
};
// Both Json and Bytes consume the body
// You can only use one extractor per request
#[derive(serde::Deserialize)]
struct Data {
value: String,
}
// This works: Json consumes body
async fn json_only(Json(data): Json<Data>) -> String {
data.value
}
// This works: Bytes consumes body
async fn bytes_only(body: Bytes) -> String {
String::from_utf8_lossy(&body).to_string()
}
// This does NOT work: can't extract body twice
// async fn both(Json(data): Json<Data>, body: Bytes) -> String {
// // Error: body already consumed
// }Both extractors consume the request body; you can't use both.
use axum::{
body::Bytes,
Json,
http::Request,
middleware::{self, Next},
response::{IntoResponse, Response},
extension::Extension,
};
use serde::Deserialize;
#[derive(Deserialize, Clone)]
struct User {
username: String,
}
// Middleware to capture raw body
async fn capture_body_middleware(
req: Request,
next: Next,
) -> Response {
// This won't work directly because body can only be consumed once
// Need to use body cloning or other techniques
next.run(req).await
}
// Alternative: parse in middleware, store in extension
async fn parse_and_store(
body: Bytes,
) -> impl IntoResponse {
#[derive(Clone)]
struct RawBody(Bytes);
#[derive(Clone)]
struct ParsedUser(User);
// Store both - but this requires restructuring
let user: User = serde_json::from_slice(&body).unwrap();
// You'd need to use Request extensions or state to pass both
}To access both raw and parsed data, you need middleware or request extensions.
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Deserialize;
#[derive(Deserialize)]
struct User {
username: String,
}
// Default rejection returns generic error
async fn default_rejection(Json(user): Json<User>) -> String {
user.username
}
// Custom rejection handling
async fn custom_rejection(
result: Result<Json<User>, axum::extract::rejection::JsonRejection>,
) -> impl IntoResponse {
match result {
Ok(Json(user)) => (StatusCode::OK, format!("User: {}", user.username)),
Err(e) => {
let status = match e {
axum::extract::rejection::JsonRejection::JsonDataError(_) => {
StatusCode::UNPROCESSABLE_ENTITY
}
axum::extract::rejection::JsonRejection::JsonSyntaxError(_) => {
StatusCode::BAD_REQUEST
}
axum::extract::rejection::JsonRejection::MissingJsonContentType(_) => {
StatusCode::UNSUPPORTED_MEDIA_TYPE
}
_ => StatusCode::BAD_REQUEST,
};
(status, format!("Error: {}", e))
}
}
}Handle Json rejections for custom error responses.
use axum::body::Bytes;
// Bytes works with any body content
async fn handle_binary(body: Bytes) -> impl IntoResponse {
// Binary file upload
format!("Received {} bytes of binary data", body.len())
}
async fn handle_text(body: Bytes) -> impl IntoResponse {
// Plain text
let text = String::from_utf8_lossy(&body);
format!("Received text: {}", text)
}
async fn handle_form(body: Bytes) -> impl IntoResponse {
// URL-encoded form data
let form = String::from_utf8_lossy(&body);
format!("Form data: {}", form)
}
async fn handle_anything(body: Bytes) -> impl IntoResponse {
// Bytes doesn't care about content type
// Useful for proxying or forwarding
format!("Forwarding {} bytes", body.len())
}Bytes is content-agnostic, working with any body format.
use axum::{
body::Bytes,
Json,
http::StatusCode,
response::IntoResponse,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct User {
username: String,
email: String,
}
// Option 1: Use Json when you need typed data
async fn create_user_json(Json(user): Json<User>) -> impl IntoResponse {
// Automatic parsing, type-safe access
// Automatic error responses for invalid JSON
// Requires Content-Type: application/json
(StatusCode::OK, Json(user))
}
// Option 2: Use Bytes for raw access
async fn create_user_bytes(body: Bytes) -> impl IntoResponse {
// Manual parsing required
// Can log raw body
// No Content-Type enforcement
match serde_json::from_slice::<User>(&body) {
Ok(user) => (StatusCode::OK, Json(user)),
Err(e) => (StatusCode::BAD_REQUEST, e.to_string()),
}
}Choose based on your needs: type safety vs raw access.
| Feature | Json<T> | Bytes |
|---------|-----------|---------|
| Deserialization | Automatic | Manual |
| Type safety | Full | None |
| Content-Type check | Required | None |
| Error handling | Automatic 400 | Manual |
| Use case | Standard APIs | Logging, debugging, custom parsing |
| Performance | Parse overhead | Zero parse overhead |
| Body access | Struct fields | Raw bytes |
The choice between axum::Json and Bytes depends on your requirements:
Use Json<T> when:
Use Bytes when:
Key insight: Json<T> is opinionated and convenientāit enforces JSON content type, parses automatically, and provides typed access with proper error handling. Bytes is raw and flexibleāit gives you unprocessed bytes that you can handle however needed. In most API endpoints, Json is the right choice; reserve Bytes for debugging, logging middleware, or when you need custom processing that Json doesn't support.