Loading page…
Rust walkthroughs
Loading page…
axum::Json and axum::Form for structured request body extraction?axum::Json and axum::Form represent fundamentally different content negotiation strategies: Json expects application/json bodies with serde's full deserialization capabilities, while Form expects application/x-www-form-urlencoded data with simpler key-value semantics. The choice impacts content-type requirements, type conversion capabilities, nested structure support, error handling, and client compatibility. Json enables complex nested types, optional fields, and sophisticated serde attributes, but requires clients to send properly formatted JSON. Form works naturally with HTML forms and URL-encoded data, supports a subset of serde features, and has different error semantics for missing fields. Understanding these trade-offs helps choose the right extractor for each endpoint based on client type, data complexity, and API design constraints.
use axum::{
extract::Json,
Form,
routing::post,
Router,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct UserInput {
username: String,
email: String,
}
// Json extractor: expects application/json
async fn create_json(Json(user): Json<UserInput>) -> String {
format!("Created user: {} <{}>", user.username, user.email)
}
// Form extractor: expects application/x-www-form-urlencoded
async fn create_form(Form(user): Form<UserInput>) -> String {
format!("Created user: {} <{}>", user.username, user.email)
}
fn app() -> Router {
Router::new()
.route("/json", post(create_json))
.route("/form", post(create_form))
}
// JSON request:
// POST /json
// Content-Type: application/json
// {"username": "alice", "email": "alice@example.com"}
// Form request:
// POST /form
// Content-Type: application/x-www-form-urlencoded
// username=alice&email=alice%40example.comBoth extractors deserialized the same type, but expect different content types and body formats.
use axum::{
extract::Json,
Form,
http::{header, StatusCode},
response::IntoResponse,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct Input {
value: String,
}
async fn json_endpoint(Json(input): Json<Input>) -> impl IntoResponse {
format!("JSON: {}", input.value)
}
async fn form_endpoint(Form(input): Form<Input>) -> impl IntoResponse {
format!("Form: {}", input.value)
}
// Content-Type behavior:
// Json requires application/json:
// POST /json
// Content-Type: application/json
// {"value": "test"}
// ✅ Success: Input { value: "test" }
// POST /json
// Content-Type: text/plain
// {"value": "test"}
// ❌ Rejection: expected application/json
// Form requires application/x-www-form-urlencoded:
// POST /form
// Content-Type: application/x-www-form-urlencoded
// value=test
// ✅ Success
// POST /form
// Content-Type: application/json
// {"value": "test"}
// ❌ Rejection: expected form-urlencodedEach extractor validates the Content-Type header and rejects requests with incompatible types.
use axum::{Json, Form};
use serde::Deserialize;
#[derive(Deserialize)]
struct ComplexInput {
name: String,
count: i32,
ratio: f64,
active: bool,
// Nested structures work differently for each
tags: Vec<String>,
}
// JSON handles all types naturally:
// {"name": "test", "count": 42, "ratio": 3.14, "active": true, "tags": ["a", "b"]}
// Form has limitations:
// name=test&count=42&ratio=3.14&active=true&tags=a&tags=b
// Note: tags requires repeated keys or special handling
// For complex nested types:
#[derive(Deserialize)]
struct NestedInput {
user: UserInfo,
settings: Settings,
}
#[derive(Deserialize)]
struct UserInfo {
name: String,
email: String,
}
#[derive(Deserialize)]
struct Settings {
notifications: bool,
theme: String,
}
// JSON handles nested objects naturally:
// {"user": {"name": "Alice", "email": "alice@example.com"},
// "settings": {"notifications": true, "theme": "dark"}}
// Form requires special encoding for nested structures:
// user[name]=Alice&user[email]=alice@example.com&settings[notifications]=true&settings[theme]=dark
// This works with serde's nested form support but is more limitedJSON naturally supports complex nested structures; Form requires special encoding and has limitations.
use axum::{Json, Form};
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct OptionalFields {
required: String,
optional: Option<String>,
with_default: String,
}
impl Default for OptionalFields {
fn default() -> Self {
Self {
required: String::new(),
optional: None,
with_default: "default_value".to_string(),
}
}
}
// JSON with serde attributes for defaults:
#[derive(Deserialize, Debug)]
struct JsonInput {
required: String,
#[serde(default)]
optional: Option<String>,
#[serde(default = "default_value")]
with_default: String,
}
fn default_value() -> String {
"default_value".to_string()
}
// JSON: Missing fields use defaults/None
// {"required": "test"}
// Result: JsonInput { required: "test", optional: None, with_default: "default_value" }
// Form: Similar but with different missing key semantics
// required=test
// Result: optional=None, with_default uses serde defaultBoth extractors support #[serde(default)] and optional fields, but missing field handling differs.
use axum::{
extract::{rejection::JsonRejection, Form, FormRejection},
Json,
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct Input {
count: i32,
}
// JSON rejection types:
async fn json_with_error(Json(input): Json<Input>) -> Result<String, JsonRejection> {
Ok(format!("Count: {}", input.count))
}
// Form rejection types:
async fn form_with_error(Form(input): Form<Input>) -> Result<String, FormRejection> {
Ok(format!("Count: {}", input.count))
}
// JSON rejections:
// - JsonRejection::MissingJsonContentType: wrong content-type
// - JsonRejection::JsonDataError: malformed JSON
// - JsonRejection::JsonSyntaxError: syntax error
// - JsonRejection::BytesRejection: body read error
// Form rejections:
// - FormRejection::MissingContentType: no content-type
// - FormRejection::InvalidFormContentType: wrong content-type
// - FormRejection::FailedToDeserializeForm: parse error
// - FormRejection::FailedToDeserializeFormBody: body parse error
// Custom error handling:
async fn custom_error_handler(
result: Result<Json<Input>, JsonRejection>,
) -> impl IntoResponse {
match result {
Ok(Json(input)) => (StatusCode::OK, format!("OK: {}", input.count)).into_response(),
Err(JsonRejection::MissingJsonContentType(_)) => {
(StatusCode::UNSUPPORTED_MEDIA_TYPE, "Expected application/json")
}
Err(JsonRejection::JsonDataError(_)) => {
(StatusCode::BAD_REQUEST, "Invalid JSON data")
}
Err(e) => (StatusCode::BAD_REQUEST, e.to_string()),
}
}Each extractor has specific rejection types reflecting different failure modes.
use axum::{
extract::Json,
Form,
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct StrictInput {
id: u64,
name: String,
}
// JSON: Invalid type values
async fn json_strict(Json(input): Json<StrictInput>) -> impl IntoResponse {
format!("ID: {}, Name: {}", input.id, input.name)
}
// Request: {"id": "not_a_number", "name": "test"}
// JSON returns error: type mismatch (serde_json error)
// Form: Different parsing behavior
// id=not_a_number&name=test
// Form returns error: failed to deserialize form
// JSON: Extraneous fields
// {"id": 1, "name": "test", "extra": "ignored"}
// Default: ignores unknown fields (serde default)
// With #[serde(deny_unknown_fields)]: returns error
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictJsonInput {
id: u64,
name: String,
}
// Form: URL-encoded quirks
// Multiple values for same key: tags=a&tags=b
// Works for Vec<String> fields
// Empty strings:
// name=&id=1
// JSON: {"name": ""} -> empty string
// Form: name= -> empty string (same behavior)Both extractors fail on type mismatches, but JSON's serde_json offers more detailed error messages.
use axum::{Json, Form};
use serde::Deserialize;
// Deep nesting works naturally in JSON:
#[derive(Deserialize)]
struct DeepJsonInput {
level1: Level1,
}
#[derive(Deserialize)]
struct Level1 {
level2: Level2,
}
#[derive(Deserialize)]
struct Level2 {
value: String,
}
// JSON request:
// {
// "level1": {
// "level2": {
// "value": "nested"
// }
// }
// }
// Form with nested structures:
#[derive(Deserialize)]
struct DeepFormInput {
#[serde(rename = "level1[level2][value]")]
nested_value: String,
}
// Or using serde's flatten:
#[derive(Deserialize)]
struct FormInput {
#[serde(flatten)]
level1: Level1,
}
// Form encoding for nested:
// level1[level2][value]=nested
// This is bracket notation, supported by serde_urlencoded
// Limitations:
// - Array of objects is difficult in form encoding
// - Deep nesting becomes unwieldy
// - JSON is much cleaner for complex structuresJSON handles deep nesting naturally; Form requires bracket notation and has practical limits.
use axum::{Json, Form};
use serde::Deserialize;
#[derive(Deserialize)]
struct ArrayInput {
items: Vec<String>,
numbers: Vec<i32>,
}
// JSON arrays:
// {"items": ["a", "b", "c"], "numbers": [1, 2, 3]}
// Straightforward array syntax
// Form arrays:
// items=a&items=b&items=c&numbers=1&numbers=2&numbers=3
// Repeated keys for arrays
// Form with serde_urlencoded supports:
// - Repeated keys: key=val1&key=val2
// - Indexed: key[0]=val1&key[1]=val2
// But complex arrays of objects are awkward:
#[derive(Deserialize)]
struct ComplexArray {
users: Vec<User>,
}
#[derive(Deserialize)]
struct User {
name: String,
email: String,
}
// JSON: users: [{"name": "a", "email": "a@b.com"}, ...]
// Form: users[0][name]=a&users[0][email]=a@b.com&users[1][name]=b...
// Very verbose and error-prone for clientsJSON arrays are straightforward; Form requires repeated keys or bracket notation, and arrays of objects are unwieldy.
use axum::{
extract::{Json, Form, BodyStream},
body::Bytes,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct Input {
value: String,
}
// Json and Form consume the body
// To access raw body, use different extractors:
// For streaming (large bodies):
async fn stream_body(body: BodyStream) {
use futures::StreamExt;
let mut stream = body;
while let Some(chunk) = stream.next().await {
let chunk: Result<Bytes, _> = chunk;
// Process chunk by chunk
}
}
// For small bodies, get bytes:
async fn raw_bytes(bytes: Bytes) -> String {
// Raw bytes, no parsing
String::from_utf8_lossy(&bytes).to_string()
}
// If you need both:
async fn inspect_and_parse(bytes: Bytes) -> Result<String, axum::extract::rejection::JsonRejection> {
// First inspect raw bytes
println!("Raw body: {:?}", bytes);
// Then parse as JSON
let input: Input = serde_json::from_slice(&bytes)
.map_err(axum::extract::rejection::JsonRejection::from)?;
Ok(input.value)
}
// Form requires application/x-www-form-urlencoded parsing:
async fn parse_form_bytes(bytes: Bytes) -> Result<Input, axum::extract::rejection::FormRejection> {
let input: Input = serde_urlencoded::from_bytes(&bytes)
.map_err(axum::extract::rejection::FormRejection::from)?;
Ok(input)
}Both Json and Form consume the body; use Bytes or BodyStream for raw access.
use axum::{
extract::{Json, Form, Path, Query, State},
Router,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct PathParams {
id: u64,
}
#[derive(Deserialize)]
struct QueryParams {
filter: Option<String>,
}
#[derive(Deserialize)]
struct BodyInput {
name: String,
}
#[derive(Clone)]
struct AppState {
db: String,
}
// JSON with other extractors:
async fn json_combined(
Path(PathParams { id }): Path<PathParams>,
Query(QueryParams { filter }): Query<QueryParams>,
Json(BodyInput { name }): Json<BodyInput>,
State(AppState { db }): State<AppState>,
) -> String {
format!("id={}, filter={:?}, name={}, db={}", id, filter, name, db)
}
// Form with other extractors:
async fn form_combined(
Path(PathParams { id }): Path<PathParams>,
Query(QueryParams { filter }): Query<QueryParams>,
Form(BodyInput { name }): Form<BodyInput>,
) -> String {
format!("id={}, filter={:?}, name={}", id, filter, name)
}
// Both work with other extractors
// The difference is only in body parsingBoth extractors work identically when combined with Path, Query, and State extractors.
use axum::{
Json, Form,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
struct FormData {
username: String,
password: String,
}
// HTML forms naturally produce form-encoded data:
// <form action="/login" method="post">
// <input name="username" />
// <input name="password" />
// </form>
// POST /login with Content-Type: application/x-www-form-urlencoded
// Use Form for HTML form submissions:
async fn login_form(Form(data): Form<FormData>) -> String {
format!("Login attempt: {}", data.username)
}
// JavaScript fetch API can send both:
// JSON approach:
async fn login_json(Json(data): Json<FormData>) -> String {
format!("Login attempt: {}", data.username)
}
// JavaScript for JSON:
// fetch('/login', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ username: 'user', password: 'pass' })
// })
// JavaScript for Form:
// fetch('/login', {
// method: 'POST',
// headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
// body: 'username=user&password=pass'
// })
// Or use FormData:
// const formData = new FormData();
// formData.append('username', 'user');
// formData.append('password', 'pass');
// fetch('/login', { method: 'POST', body: formData })Choose Form for HTML forms and traditional web clients; Json for APIs consumed by JavaScript.
use axum::{
extract::{Json, Form, Multipart},
Router,
};
use serde::Deserialize;
// Neither Json nor Form handle multipart/form-data
// Use Multipart extractor for file uploads:
#[derive(Deserialize)]
struct FileMetadata {
name: String,
description: String,
}
// For multipart with files:
async fn upload_multipart(mut multipart: Multipart) -> String {
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
if name == "file" {
let data = field.bytes().await.unwrap();
// Process file data
} else if name == "metadata" {
let data = field.text().await.unwrap();
let metadata: FileMetadata = serde_json::from_str(&data).unwrap();
// Process metadata
}
}
"Uploaded".to_string()
}
// Multipart Content-Type: multipart/form-data; boundary=----...
// This is different from both application/json and application/x-www-form-urlencoded
// If you need simple file uploads with form data:
// Form only handles simple key-value, not files
// Json only handles JSON, not files
// Multipart handles both files and form fieldsFor file uploads, use Multipart; neither Json nor Form handles multipart/form-data.
use axum::{Json, Form};
use serde::Deserialize;
#[derive(Deserialize)]
struct UserInput {
username: String,
email: String,
}
// JSON: Can validate with serde attributes
#[derive(Deserialize)]
#[serde(deny_unknown_fields)] // Reject unknown fields
struct SecureJsonInput {
#[serde(rename = "username")]
username: String,
#[serde(default)] // Optional with default
role: String,
}
// Form: URL encoding can have security implications
// - URL encoding attacks
// - Size limits (URLs have practical limits)
// - Sensitive data in URL-encoded body (less visible than query params)
// Content-Type validation:
// Json strictly requires Content-Type: application/json
// Form accepts Content-Type: application/x-www-form-urlencoded
// CSRF considerations:
// HTML forms typically need CSRF tokens
// JSON API requests often use different auth (bearer tokens, etc.)
// Example with CSRF:
#[derive(Deserialize)]
struct CsrfProtectedForm {
#[serde(rename = "_csrf")]
csrf_token: String,
username: String,
}
async fn protected_form(Form(input): Form<CsrfProtectedForm>) -> String {
// Validate CSRF token first
format!("User: {}", input.username)
}Consider CSRF protection for form endpoints; JSON APIs often use different authentication patterns.
use axum::{Json, Form};
use serde::Deserialize;
#[derive(Deserialize)]
struct LargeInput {
data: Vec<String>,
}
// JSON parsing:
// - Uses serde_json
// - Allocates intermediate structures
// - Can parse streaming JSON
// - Handles arbitrary precision numbers
// - Optimized for large documents
// Form parsing:
// - Uses serde_urlencoded
// - Simpler parsing (key=value pairs)
// - Less overhead for simple structures
// - Limited nesting support
// - More efficient for small, flat data
// Benchmark considerations:
// - JSON: O(n) parsing with allocation
// - Form: O(n) parsing, simpler state machine
// For large bodies, both need streaming:
// - JSON: Use Json extractor with size limits
// - Form: URL-encoded bodies are typically smaller
// Memory implications:
// - JSON: May hold entire string in memory during parsing
// - Form: Can stream-parse URL-encoded data more easilyForm parsing has less overhead for simple data; JSON parsing handles complex structures more efficiently.
use axum::{Json, Form};
use serde::Deserialize;
#[derive(Deserialize)]
struct Example {
field: String,
}
// | Aspect | Json | Form |
// |---------------------|----------------------------------|-----------------------------------|
// | Content-Type | application/json | application/x-www-form-urlencoded |
// | Nested structures | Full support | Limited (bracket notation) |
// | Arrays | Native syntax | Repeated keys |
// | Arrays of objects | Simple | Verbose |
// | Client type | APIs, JavaScript | HTML forms |
// | Error detail | Detailed JSON errors | Simpler form errors |
// | Serde attributes | All supported | Subset supported |
// | Type coercion | Strict JSON types | String conversion |
// | File uploads | No | No (use Multipart) |
// | Size efficiency | More compact for complex data | More verbose |
// | Security | Needs auth headers | Often needs CSRF |
// | Unknown fields | Ignored by default | Ignored by default |
// Choose Json when:
// - Building REST APIs
// - Complex nested structures
// - JavaScript clients
// - Need full serde features
// Choose Form when:
// - HTML form submissions
// - Simple flat structures
// - Traditional web apps
// - URL-encoded data from clientsThe choice depends on client type, data complexity, and API design patterns.
use axum::{
Json, Form,
routing::post,
Router,
};
use serde::{Deserialize, Serialize};
// API endpoint pattern: Use Json
#[derive(Deserialize, Serialize)]
struct ApiRequest {
user: User,
settings: Settings,
}
#[derive(Deserialize, Serialize)]
struct User {
name: String,
email: String,
}
#[derive(Deserialize, Serialize)]
struct Settings {
notifications: bool,
theme: String,
}
async fn api_endpoint(Json(req): Json<ApiRequest>) -> Json<ApiResponse> {
Json(ApiResponse { success: true })
}
#[derive(Serialize)]
struct ApiResponse {
success: bool,
}
// Web form endpoint pattern: Use Form
#[derive(Deserialize)]
struct LoginFormData {
username: String,
password: String,
#[serde(rename = "_csrf")]
csrf_token: String,
}
async fn login_form(Form(data): Form<LoginFormData>) -> &'static str {
// Validate CSRF, authenticate
"Logged in"
}
// Hybrid approach: Accept both
async fn hybrid_endpoint(
json: Option<Json<ApiRequest>>,
form: Option<Form<ApiRequest>>,
) -> &'static str {
let data = json.map(|Json(d)| d)
.or(form.map(|Form(f)| f));
match data {
Some(data) => "Processed",
None => "No data",
}
}
fn app() -> Router {
Router::new()
.route("/api", post(api_endpoint)) // JSON API
.route("/login", post(login_form)) // HTML form
.route("/hybrid", post(hybrid_endpoint)) // Accepts both
}Match the extractor to the expected client and data structure complexity.