Serde JSON in Rust: serialize and deserialize structs
How to convert Rust structs to and from JSON with serde and serde_json: derive, to_string, from_str, rename, Option fields, defaults and errors.
serde (SERialize/DEserialize) is the standard Rust framework for converting data to and from formats like JSON, TOML and YAML. You describe your data as normal structs and enums, derive two traits, and a format crate such as serde_json does the rest.
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Struct to JSON and back
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Player {
name: String,
level: u32,
tags: Vec<String>,
}
fn main() {
let p = Player { name: "Ferris".into(), level: 7, tags: vec!["crab".into()] };
let json = serde_json::to_string(&p).unwrap();
println!("{json}");
let back: Player = serde_json::from_str(r#"{"name":"Corro","level":3,"tags":[]}"#).unwrap();
println!("{back:?}");
}
{"name":"Ferris","level":7,"tags":["crab"]}
Player { name: "Corro", level: 3, tags: [] }
serde_json::to_string(&value)produces compact JSON;to_string_prettyadds indentation.serde_json::from_str::<T>(text)parses. It needs the target type, from an annotation or turbofish.- Both return a
Result: serialization rarely fails, but parsing outside data fails all the time.
r#"..."# is a raw string: quotes inside don't need escaping, which is handy for JSON in code.
Shaping the JSON with attributes
| Attribute | Effect |
|---|---|
#[serde(rename_all = "camelCase")] on the struct |
user_id becomes "userId" |
#[serde(rename = "type")] on a field |
use a JSON name that's a Rust keyword |
Option<T> field |
null or a missing key becomes None |
#[serde(default)] |
missing key uses Default::default() |
#[serde(skip_serializing_if = "Option::is_none")] |
leave out None fields when writing |
#[serde(skip)] |
never read or write this field |
#[serde(tag = "type")] on an enum |
internally tagged: {"type": "Click", "x": 1} |
#[serde(deny_unknown_fields)] |
reject keys you didn't expect |
By default, unknown JSON keys are ignored, and a missing non-Option field is an error.
Handling errors
serde_json::Error says what went wrong and where:
missing field `level` at line 1 column 17invalid type: string "seven", expected u32 at line 1 column 15expected value at line 1 column 1(not JSON at all)
Match on the Result, or propagate with ? into your error type (with anyhow, add context: .context("parsing config.json")).
Untyped JSON: Value and json!
When the shape isn't known in advance, parse into serde_json::Value and index it: v["user"]["name"].as_str(). The json! macro builds a Value with JSON-like syntax. Prefer typed structs for anything you'll use more than once: the compiler then checks every field access.
Common mistakes
- Forgetting the derive feature:
cannot find derive macro `Serialize` in this scope. Enablefeatures = ["derive"]onserde. - Field names not matching the JSON's naming style: use
rename_allinstead of renaming every Rust field. unwrap()onfrom_strwith user data. One bad request crashes the server. Return the error.- Floats for money.
19.99in JSON parses into anf64with rounding error; use integer cents or a decimal crate. - Expecting map key order from a
HashMapfield. UseBTreeMapfor stable output.
Example
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Order {
order_id: u64,
customer: String,
items: Vec<Item>,
#[serde(default)]
gift_wrap: bool,
#[serde(skip_serializing_if = "Option::is_none")]
coupon: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Item {
sku: String,
qty: u32,
price_cents: u64,
}
fn main() {
let input = r#"{
"orderId": 1042,
"customer": "Ferris",
"items": [
{"sku": "MUG-01", "qty": 2, "price_cents": 1250},
{"sku": "TEE-XL", "qty": 1, "price_cents": 2000}
]
}"#;
let order: Order = serde_json::from_str(input).expect("valid order JSON");
let total: u64 = order.items.iter().map(|i| u64::from(i.qty) * i.price_cents).sum();
println!("order {} for {}: {} items, total {total} cents, gift wrap {}", order.order_id, order.customer, order.items.len(), order.gift_wrap);
println!("first sku {}", order.items[0].sku);
println!("{}", serde_json::to_string(&order).unwrap());
for bad in [r#"{"orderId": 1}"#, r#"{"orderId": "x", "customer": "a", "items": []}"#, "<html>502</html>"] {
match serde_json::from_str::<Order>(bad) {
Ok(o) => println!("parsed {:?}", o.coupon),
Err(e) => println!("error: {e}"),
}
}
let event = json!({ "type": "checkout", "order": order.order_id, "ok": true });
println!("{} {}", event, event["order"].as_u64().unwrap_or(0));
}
Output:
order 1042 for Ferris: 2 items, total 4500 cents, gift wrap false
first sku MUG-01
{"orderId":1042,"customer":"Ferris","items":[{"sku":"MUG-01","qty":2,"price_cents":1250},{"sku":"TEE-XL","qty":1,"price_cents":2000}],"giftWrap":false}
error: missing field `customer` at line 1 column 14
error: invalid type: string "x", expected u64 at line 1 column 15
error: expected value at line 1 column 1
{"ok":true,"order":1042,"type":"checkout"} 1042