Expert · Real projects: CLI, web and WebAssembly · topic 44 of 50 · 25 min
Serde and JSON
Explanation
Why serde?
Programs constantly exchange data: web APIs send JSON, config files use TOML or YAML, caches store binary data. Serialisation means turning a value in memory into text or bytes; deserialisation is the reverse. serde (SERialise/DEserialise) is the crate almost every Rust project uses for this. It is fast, and it turns messy text into typed structs, so the rest of your code never deals with raw strings.
cargo add serde --features derive
cargo add serde_jsonStruct to JSON and back
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Order {
id: u32,
item: String,
qty: u32,
paid: bool,
}
fn main() -> Result<(), serde_json::Error> {
let o = Order { id: 7, item: String::from("thali"), qty: 2, paid: true };
let text = serde_json::to_string(&o)?;
println!("{text}"); // {"id":7,"item":"thali","qty":2,"paid":true}
let back: Order = serde_json::from_str(r#"{"id":8,"item":"lassi","qty":1,"paid":false}"#)?;
println!("{:?}", back);
Ok(())
}Serialize and Deserialize are traits (module 7). The derive writes their code for you from the struct's fields at compile time, so there is no slow run-time inspection of your types. The same derive works for every format crate: serde_json for JSON, toml for TOML files, and others, which is why learning serde once pays off everywhere.
from_str checks everything: if a field is missing, has the wrong type, or the JSON is broken, you get an Err with a clear message such as "missing field `qty` at line 1 column 30". Once you have an Order, every field is guaranteed to be valid.
Matching other naming styles
JavaScript APIs usually use camelCase (orderId), while Rust uses snake_case (order_id). Attributes tell serde how to map names, so your Rust code stays idiomatic.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Payment {
order_id: u32, // JSON: "orderId"
#[serde(rename = "txnRef")]
reference: String, // JSON: "txnRef"
upi_id: Option<String>, // may be missing or null
#[serde(default)]
retries: u32, // missing -> 0
}Enums and nested data
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
enum Status { Placed, Shipped, Delivered }
#[derive(Serialize, Deserialize, Debug)]
struct Tracking {
id: u32,
status: Status, // "shipped"
stops: Vec<String>, // JSON array
}Unknown shape: serde_json::Value
When you do not know the structure in advance, read into serde_json::Value, a type that can hold any JSON. Prefer typed structs whenever you can; they catch mistakes much earlier.
let v: serde_json::Value = serde_json::from_str(r#"{"city":"Pune","temp":31}"#)?;
println!("{}", v["city"]); // "Pune"
println!("{}", v["temp"].as_i64().unwrap_or(0) + 1); // 32Common mistakes
- Forgetting the derive feature: "cannot find derive macro Serialize". Use cargo add serde --features derive.
- Making every field Option "to be safe". Then the compiler cannot help you; only use Option for truly optional data.
- Using f64 for money in JSON. Send paise as an integer or a string.
- Ignoring the error from from_str. Show it: it says exactly which field and position failed.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Serde and JSON · 1/6
serde converts Rust values to and from formats like JSON. Almost every Rust project uses it.
Prefer a video by a teacher? Find free videos on “rust serde json serialize deserialize tutorial” on YouTube ↗
In real life: Reading a weather API response
A farming advice app calls a weather API that returns JSON with camelCase names and some optional fields. The Rust backend defines a Forecast struct with rename_all = "camelCase" and Option for rain probability. If the API changes a field's type one day, deserialisation fails with a clear error in the logs instead of silently showing farmers wrong numbers.
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Forecast {
district: String,
max_temp_c: i32,
rain_chance: Option<u8>,
}
fn main() {
let json = r#"{"district":"Nashik","maxTempC":33,"rainChance":null}"#;
match serde_json::from_str::<Forecast>(json) {
Ok(f) => println!("{} {} {:?}", f.district, f.max_temp_c, f.rain_chance),
Err(e) => eprintln!("bad data: {e}"),
}
}Practice (0/3 done)
What does this print? #[derive(Serialize)] struct P { name: String, age: u8 } let p = P { name: "Ira".into(), age: 9 }; println!("{}", serde_json::to_string(&p).unwrap());
The JSON has "orderId" but your field is order_id. What fixes it?
Define structs for a train booking JSON: {"pnr":"4512378901","trainNo":12951,"passengers":[{"name":"Asha","age":34,"berth":"LB"},{"name":"Om","age":8,"berth":null}]}. Deserialize it, print each passenger, add one passenger, and serialize it back with to_string_pretty.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Rust for the web with axumReal projects: CLI, web and WebAssembly · same module
- Rust and WebAssemblyReal projects: CLI, web and WebAssembly · same module
- Files and JSON with System.Text.JsonC# and .NET · Exceptions, delegates, events and async
- JSON with encoding/jsonGo Programming (backend and cloud) · Standard library essentials
- JSON: the language of APIsBackend Development · How the web and backends work
- Pointers to structs and the arrow operatorC Programming (from zero to systems) · Structs, unions, enums and files
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does deserialisation mean?
Answer: Turning text or bytes back into a typed value
Deserialise = read external data into your types.
A JSON field may be null or missing. Which field type fits?
Answer: Option<String>
Option maps null and missing keys to None.
What does from_str return when a required field is missing?
Answer: An Err describing the missing field
serde reports exactly what is wrong.
Which feature must be enabled on serde for #[derive(Serialize)]?
Answer: derive
cargo add serde --features derive.
What does JsonSerializer.Deserialize<T> throw for text that is not valid JSON?Related
After let b = Student { roll: 2, ..a }; where a has String fields, what is true?Related
Answer: a's String fields were moved into b
Struct update syntax moves non-Copy fields.
What happens to a JSON key that has no matching struct field?Related
Answer: It is ignored, unless the Decoder uses DisallowUnknownFields
By default unknown keys are skipped.
What can an enum variant hold in Rust?Related
Answer: Any data: none, a tuple of values, or named fields
Rust variants can be unit-like, tuple-like or struct-like.
From Enums and Option
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Real projects: CLI, web and WebAssembly” before its exam: Rust for the web with axum, Rust and WebAssembly.
- Module 11 exam: Real projects · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Rust careers and interviews. 50 topics left in the course before the final exam.