serde(flatten) for Composing JSON Shapes
Use #[serde(flatten)] to inline an embedded struct's fields into the parent JSON object. Cleaner code reuse for shared envelopes.
Detailed Explanation
Flattening Embedded Structs into Their Parent
#[serde(flatten)] tells serde to merge an embedded struct's fields into the parent's JSON object instead of nesting them under a key. It is the standard way to share an envelope of common fields across many DTOs.
The problem
Suppose every API response shares a request_id and timestamp:
{
"request_id": "abc123",
"timestamp": "2024-06-15T10:00:00Z",
"user": { "id": 1, "name": "Ada" }
}
Without flatten, you would have to repeat request_id and timestamp in every response struct. Tedious and error-prone.
With flatten
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
pub request_id: String,
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserResponse {
#[serde(flatten)]
pub envelope: Envelope,
pub user: User,
}
The wire JSON has request_id, timestamp, and user at the top level — exactly as the API specifies. The Rust code accesses them as response.envelope.request_id.
Catch-all map
flatten also works with a final HashMap field to collect any leftover keys:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub event_type: String,
#[serde(flatten)]
pub extra: std::collections::HashMap<String, serde_json::Value>,
}
Now any keys not explicitly named in the struct land in extra, preserving forward compatibility when the upstream service adds new fields.
Caveats
flattencannot be combined withdeny_unknown_fieldson the parent — the catch-all behaviour conflicts with strict validation.- It is slightly slower than nested structs because serde performs a two-pass deserialization, buffering fields and re-routing them.
- Numeric tag fields cannot be flattened reliably; serde requires unique key names across the merged set.
When to reach for it
Use flatten whenever multiple DTOs share a common header, footer, or metadata block. It keeps the Rust types DRY without distorting the JSON wire format.
Use Case
API response envelopes, JSON:API spec bodies, GraphQL extensions, and event metadata blocks all benefit from flatten. It enables real composition without forcing the wire format to nest your shared fields.