Loading page…
Rust walkthroughs
Loading page…
serde's #[serde(flatten)] to compose nested structures without affecting the serialized format?The #[serde(flatten)] attribute allows you to nest structs within your data types while keeping the serialized output flat. This enables better code organization through composition without changing how data appears in JSON or other formats. The inner struct's fields are "flattened" into the parent structure during serialization.
use serde::{Serialize, Deserialize};
// You want to organize related fields into a struct
#[derive(Serialize, Deserialize)]
struct User {
// Personal info
name: String,
email: String,
age: u32,
// Address info
street: String,
city: String,
country: String,
// Settings
notifications_enabled: bool,
theme: String,
}
// JSON output is flat:
// {"name":"Alice","email":"alice@example.com","age":30,
// "street":"123 Main St","city":"Boston","country":"USA",
// "notifications_enabled":true,"theme":"dark"}The fields are flat in JSON but grouped logically in code.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Address {
street: String,
city: String,
country: String,
}
#[derive(Serialize, Deserialize)]
struct Settings {
notifications_enabled: bool,
theme: String,
}
#[derive(Serialize, Deserialize)]
struct User {
name: String,
email: String,
age: u32,
address: Address, // Nested struct
settings: Settings, // Nested struct
}
fn main() {
let user = User {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
age: 30,
address: Address {
street: "123 Main St".to_string(),
city: "Boston".to_string(),
country: "USA".to_string(),
},
settings: Settings {
notifications_enabled: true,
theme: "dark".to_string(),
},
};
let json = serde_json::to_string_pretty(&user).unwrap();
println!("{}", json);
}
// JSON output has nested objects:
// {
// "name": "Alice",
// "email": "alice@example.com",
// "age": 30,
// "address": {
// "street": "123 Main St",
// "city": "Boston",
// "country": "USA"
// },
// "settings": {
// "notifications_enabled": true,
// "theme": "dark"
// }
// }Without flatten, the JSON structure mirrors the Rust structure exactly.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Address {
street: String,
city: String,
country: String,
}
#[derive(Serialize, Deserialize)]
struct Settings {
notifications_enabled: bool,
theme: String,
}
#[derive(Serialize, Deserialize)]
struct User {
name: String,
email: String,
age: u32,
#[serde(flatten)]
address: Address,
#[serde(flatten)]
settings: Settings,
}
fn main() {
let user = User {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
age: 30,
address: Address {
street: "123 Main St".to_string(),
city: "Boston".to_string(),
country: "USA".to_string(),
},
settings: Settings {
notifications_enabled: true,
theme: "dark".to_string(),
},
};
let json = serde_json::to_string_pretty(&user).unwrap();
println!("{}", json);
}
// JSON output is flat:
// {
// "name": "Alice",
// "email": "alice@example.com",
// "age": 30,
// "street": "123 Main St",
// "city": "Boston",
// "country": "USA",
// "notifications_enabled": true,
// "theme": "dark"
// }The nested struct fields are flattened into the parent object.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Metadata {
created_at: String,
updated_at: String,
version: u32,
}
#[derive(Serialize, Deserialize, Debug)]
struct Document {
id: String,
title: String,
content: String,
#[serde(flatten)]
metadata: Metadata,
}
fn deserialize_example() {
let json = r#"{
"id": "doc-123",
"title": "My Document",
"content": "Hello, world!",
"created_at": "2024-01-15",
"updated_at": "2024-01-20",
"version": 2
}"#;
let doc: Document = serde_json::from_str(json).unwrap();
println!("ID: {}", doc.id);
println!("Title: {}", doc.title);
println!("Created: {}", doc.metadata.created_at);
println!("Version: {}", doc.metadata.version);
}Deserialization extracts fields from the flat JSON into the nested struct.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Timestamps {
created_at: i64,
updated_at: i64,
}
#[derive(Serialize, Deserialize)]
struct Ownership {
created_by: String,
updated_by: String,
}
#[derive(Serialize, Deserialize)]
struct Permissions {
public_read: bool,
public_write: bool,
}
#[derive(Serialize, Deserialize)]
struct File {
name: String,
size: u64,
#[serde(flatten)]
timestamps: Timestamps,
#[serde(flatten)]
ownership: Ownership,
#[serde(flatten)]
permissions: Permissions,
}
// All flattened fields appear at the same level in JSONMultiple structs can be flattened into the same parent.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Pagination {
page: u32,
per_page: u32,
total: u32,
}
#[derive(Serialize, Deserialize)]
struct ApiResponse {
success: bool,
message: String,
#[serde(flatten)]
pagination: Pagination,
// This remains nested
data: Vec<String>,
}
fn main() {
let response = ApiResponse {
success: true,
message: "OK".to_string(),
pagination: Pagination {
page: 1,
per_page: 10,
total: 100,
},
data: vec!["item1".to_string(), "item2".to_string()],
};
let json = serde_json::to_string_pretty(&response).unwrap();
println!("{}", json);
}
// JSON:
// {
// "success": true,
// "message": "OK",
// "page": 1,
// "per_page": 10,
// "total": 100,
// "data": ["item1", "item2"]
// }You can mix flattened and non-flattened fields.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum EventData {
UserCreated { user_id: String, name: String },
UserDeleted { user_id: String },
FileUploaded { file_id: String, filename: String },
}
#[derive(Serialize, Deserialize)]
struct Event {
timestamp: i64,
#[serde(flatten)]
data: EventData,
}
fn main() {
let event = Event {
timestamp: 1705312800,
data: EventData::UserCreated {
user_id: "u-123".to_string(),
name: "Alice".to_string(),
},
};
let json = serde_json::to_string_pretty(&event).unwrap();
println!("{}", json);
}
// JSON:
// {
// "timestamp": 1705312800,
// "type": "UserCreated",
// "user_id": "u-123",
// "name": "Alice"
// }Enums with #[serde(tag = "...")] flatten their variant fields.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Default)]
struct OptionalFields {
#[serde(skip_serializing_if = "Option::is_none")]
nickname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
bio: Option<String>,
}
#[derive(Serialize, Deserialize)]
struct Profile {
username: String,
email: String,
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
optional: Option<OptionalFields>,
}
fn optional_flatten() {
// Without optional fields
let profile1 = Profile {
username: "alice".to_string(),
email: "alice@example.com".to_string(),
optional: None,
};
let json1 = serde_json::to_string(&profile1).unwrap();
println!("{}", json1);
// {"username":"alice","email":"alice@example.com"}
// With optional fields
let profile2 = Profile {
username: "bob".to_string(),
email: "bob@example.com".to_string(),
optional: Some(OptionalFields {
nickname: Some("Bobby".to_string()),
bio: None,
}),
};
let json2 = serde_json::to_string(&profile2).unwrap();
println!("{}", json2);
// {"username":"bob","email":"bob@example.com","nickname":"Bobby"}
}Optional flattened structs can be skipped entirely.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Default)]
struct ConfigDefaults {
#[serde(default)]
timeout: u32,
#[serde(default)]
retries: u32,
#[serde(default)]
debug: bool,
}
#[derive(Serialize, Deserialize)]
struct Config {
name: String,
#[serde(flatten)]
#[serde(default)]
defaults: ConfigDefaults,
}
fn default_flatten() {
// Minimal JSON - missing fields get defaults
let json = r#"{"name":"my-service"}"#;
let config: Config = serde_json::from_str(json).unwrap();
println!("Name: {}", config.name);
println!("Timeout: {}", config.defaults.timeout); // 0 (default)
println!("Retries: {}", config.defaults.retries); // 0 (default)
println!("Debug: {}", config.defaults.debug); // false (default)
}Defaults work naturally with flattened structs.
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize)]
struct KnownFields {
id: String,
name: String,
}
#[derive(Serialize, Deserialize)]
struct FlexibleRecord {
#[serde(flatten)]
known: KnownFields,
#[serde(flatten)]
extra: HashMap<String, serde_json::Value>,
}
fn capture_unknown() {
let json = r#"{
"id": "123",
"name": "Alice",
"department": "Engineering",
"location": "Boston",
"active": true
}"#;
let record: FlexibleRecord = serde_json::from_str(json).unwrap();
println!("ID: {}", record.known.id);
println!("Name: {}", record.known.name);
println!("Extra: {:?}", record.extra);
// Extra: {"department": String("Engineering"), "location": String("Boston"), "active": Bool(true)}
}Flatten with HashMap captures unknown fields.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Base {
id: String,
name: String,
}
#[derive(Serialize, Deserialize)]
struct Extended {
#[serde(flatten)]
base: Base,
// Same field name as in Base!
name: String, // This shadows the flattened 'name'
}
// During serialization, the non-flattened field wins
// During deserialization, both get the same valueBe careful with field name collisions between flattened and direct fields.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct InternalNames {
internal_id: String,
internal_name: String,
}
#[derive(Serialize, Deserialize)]
struct ExternalFormat {
#[serde(flatten)]
#[serde(rename = "prefix_")] // Note: this doesn't work on flatten
// Instead, rename in the inner struct
internal: InternalRenamed,
}
#[derive(Serialize, Deserialize)]
struct InternalRenamed {
#[serde(rename = "id")]
internal_id: String,
#[serde(rename = "name")]
internal_name: String,
}
#[derive(Serialize, Deserialize)]
struct ApiRecord {
#[serde(flatten)]
internal: InternalRenamed,
extra_field: String,
}
fn rename_example() {
let record = ApiRecord {
internal: InternalRenamed {
internal_id: "123".to_string(),
internal_name: "Alice".to_string(),
},
extra_field: "value".to_string(),
};
let json = serde_json::to_string_pretty(&record).unwrap();
println!("{}", json);
// {"id":"123","name":"Alice","extra_field":"value"}
}Rename fields in the inner struct, not on the flatten attribute.
use serde::{Serialize, Deserialize};
// Common pagination structure
#[derive(Serialize, Deserialize, Clone)]
struct Pagination {
page: u32,
per_page: u32,
total_pages: u32,
total_items: u32,
}
// Common metadata structure
#[derive(Serialize, Deserialize, Clone)]
struct Metadata {
#[serde(skip_serializing_if = "Option::is_none")]
etag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
last_modified: Option<String>,
}
// Reusable across multiple response types
#[derive(Serialize, Deserialize)]
struct PaginatedResponse<T> {
items: Vec<T>,
#[serde(flatten)]
pagination: Pagination,
#[serde(flatten)]
metadata: Metadata,
}
#[derive(Serialize, Deserialize)]
struct User {
id: String,
name: String,
}
#[derive(Serialize, Deserialize)]
struct Product {
id: String,
title: String,
price: f64,
}
fn api_example() {
// User list response
let user_response = PaginatedResponse {
items: vec![
User { id: "u1".to_string(), name: "Alice".to_string() },
User { id: "u2".to_string(), name: "Bob".to_string() },
],
pagination: Pagination {
page: 1,
per_page: 10,
total_pages: 5,
total_items: 48,
},
metadata: Metadata {
etag: Some("abc123".to_string()),
last_modified: None,
},
};
let json = serde_json::to_string_pretty(&user_response).unwrap();
println!("{}", json);
}
// JSON is flat:
// {
// "items": [{"id": "u1", "name": "Alice"}, {"id": "u2", "name": "Bob"}],
// "page": 1,
// "per_page": 10,
// "total_pages": 5,
// "total_items": 48,
// "etag": "abc123"
// }Flatten enables reusable components for API responses.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Base {
id: String,
}
#[derive(Serialize, Deserialize)]
struct Middle {
#[serde(flatten)]
base: Base,
value: i32,
}
#[derive(Serialize, Deserialize)]
struct Top {
#[serde(flatten)]
middle: Middle,
extra: String,
}
fn nested_flatten() {
let top = Top {
middle: Middle {
base: Base { id: "123".to_string() },
value: 42,
},
extra: "hello".to_string(),
};
let json = serde_json::to_string(&top).unwrap();
println!("{}", json);
// {"id":"123","value":42,"extra":"hello"}
}Flattening works recursively through nested structures.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Required {
field1: String,
field2: i32,
}
#[derive(Serialize, Deserialize, Debug)]
struct Container {
name: String,
#[serde(flatten)]
required: Required,
}
fn error_handling() {
// Missing flattened fields
let json = r#"{"name":"test"}"#;
match serde_json::from_str::<Container>(json) {
Ok(container) => println!("Parsed: {:?}", container),
Err(e) => println!("Error: {}", e),
// Error: missing field `field1` at line 1 column 15
}
// All fields present
let json = r#"{"name":"test","field1":"value","field2":42}"#;
match serde_json::from_str::<Container>(json) {
Ok(container) => println!("Parsed: {:?}", container),
Err(e) => println!("Error: {}", e),
// Parsed: Container { name: "test", required: Required { field1: "value", field2: 42 } }
}
}Error messages reference the flattened field names directly.
#[serde(flatten)] enables composition without changing serialization format:
Key benefits:
| Benefit | Description | |---------|-------------| | Code organization | Group related fields in structs | | Reuse | Share common structures across types | | API compatibility | Keep JSON flat for external APIs | | Backward compatibility | Add structure without breaking format |
Serialization behavior:
// Rust structure (nested):
Container {
name: "test",
metadata: Metadata { created: 123, updated: 456 }
}
// JSON (flat):
{"name":"test","created":123,"updated":456}Common patterns:
// Pattern 1: Shared metadata
struct Record {
id: String,
#[serde(flatten)]
metadata: Metadata, // created_at, updated_at, etc.
}
// Pattern 2: Extensible types
struct Dynamic {
#[serde(flatten)]
known: KnownFields,
#[serde(flatten)]
extra: HashMap<String, Value>,
}
// Pattern 3: Optional groups
struct Profile {
username: String,
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<ProfileDetails>,
}
// Pattern 4: Generic wrappers
struct ApiResponse<T> {
data: T,
#[serde(flatten)]
pagination: Pagination,
}Considerations:
Option for optional groupstag flatten nicely#[serde(flatten)] is essential for maintaining clean Rust code structure while working with flat JSON APIs.