Rust Serde Nested Data
Nested data is data with values inside values, like objects inside objects. Serde follows the nesting that Rust types describe.
A struct inside a struct
Types nest like the data nests. Address is a full struct in its own right.
struct Address {
city: String,
zip: String,
}
struct User {
name: String,
address: Address,
}Nested data needs no new code
Each inner type derives the same traits. Serde walks the whole tree for you.
let u = User {
name: String::from("Ada"),
address: Address { city: String::from("Paris"), zip: String::from("75001") },
};
println!("{}", serde_json::to_string(&u).unwrap());A Vec becomes a JSON array
Item here is a struct with name and qty fields. Each item becomes one object in the array. The array keeps the Vec's order.
#[derive(Serialize)]
struct Item { name: String, qty: u32 }
let items = vec![
Item { name: String::from("pen"), qty: 2 },
Item { name: String::from("pad"), qty: 1 },
];
println!("{}", serde_json::to_string(&items).unwrap());Option becomes null
Contact holds a name and an email. The email is an Option. A None writes the word null. A Some writes the value itself, with no wrapper.
#[derive(Serialize)]
struct Contact {
name: String,
email: Option<String>,
}
let a = Contact { name: String::from("Ada"), email: None };
let g = Contact { name: String::from("Grace"), email: Some(String::from("grace@example.com")) };
println!("{}", serde_json::to_string(&a).unwrap());
println!("{}", serde_json::to_string(&g).unwrap());A map becomes an object
The map keys become the object keys. JSON object keys are always text.
let mut scores = HashMap::new();
scores.insert(String::from("math"), 95);
println!("{}", serde_json::to_string(&scores).unwrap());Enums carry the variant name
By default the variant name wraps its data. Serde calls this external tagging. A variant with no data is just its name in quotes.
#[derive(Serialize)]
enum Shape {
Circle { radius: f64 },
Dot,
}
println!("{}", serde_json::to_string(&Shape::Circle { radius: 1.5 }).unwrap());
println!("{}", serde_json::to_string(&Shape::Dot).unwrap());Example
// Cargo.toml: serde = { version = "1.0.229", features = ["derive"] }, serde_json = "1.0.154"
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Serialize, Deserialize, Debug)]
struct Customer { name: String, email: Option<String> }
#[derive(Serialize, Deserialize, Debug)]
struct Line { item: String, qty: u32, price: f64 }
#[derive(Serialize, Deserialize, Debug)]
enum Payment { Cash, Card { last4: String } }
#[derive(Serialize, Deserialize, Debug)]
struct Invoice {
customer: Customer,
lines: Vec<Line>,
extras: BTreeMap<String, f64>,
payment: Payment,
}
fn main() {
let inv = Invoice {
customer: Customer { name: String::from("Ada"), email: None },
lines: vec![Line { item: String::from("pen"), qty: 2, price: 1.5 }],
extras: BTreeMap::from([(String::from("delivery"), 4.0)]),
payment: Payment::Card { last4: String::from("4242") },
};
let text = serde_json::to_string(&inv).unwrap();
println!("{text}");
let back: Invoice = serde_json::from_str(&text).unwrap();
println!("lines = {}", back.lines.len());
println!("email = {:?}", back.customer.email);
}Key points
- A missing key for an Option field is not an error.
- Serde quietly fills it with None.
- A null in the text fits only an Option field.
- Other field types reject it with an error.
- HashMap order is not promised.
- Use BTreeMap when the keys must come out sorted.
- An enum can wear other tag styles.
- The serde docs show internal tagging and adjacent tagging for APIs that demand them.
- Each level just needs its own derive.
- Nested data puts values inside other values, and the Rust types mirror that shape.
In this example, an invoice mixes every nesting shape from this lesson. The customer is a nested struct. The email is None, so it prints as null. The lines form a Vec, so they print as an array of objects. The extras live in a BTreeMap, so the keys stay sorted. The payment is an enum, so the variant name Card wraps its data. The program then reads the text back into an Invoice. One line survives the round trip. The email is still None.
Note
Nesting depth costs nothing extra.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
The JSON for one field is a list of objects. Which field type fits it best?