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.

Patterns

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

  • flatten cannot be combined with deny_unknown_fields on 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.

Try It — JSON to Rust Struct Converter

Open full tool