Step 1 of 6
From structs to JSON
Programs at work constantly swap data with other programs: a web API answers in JSON, a config file is TOML, a message on a queue is JSON again. Turning a value in memory into text (or bytes) like that is called serialization; reading it back into a value is deserialization. In Rust nearly every team uses one library for both: serde (from SERialize and DEserialize).
serde comes in two halves:
- The
serdecrate defines two traits,SerializeandDeserialize, plus derive macros that write those trait impls for your own types. - A format crate knows one data format:
serde_jsonfor JSON,tomlfor TOML, and many more. Every format works with every type that derives the traits, so you learn serde once and use it everywhere.
In a real project you add both to Cargo.toml (the derive feature switches on the derive macros):
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
On this site both are already available.
Your first JSON
use serde::Serialize;
#[derive(Serialize)]
struct Player {
name: String,
level: u32,
online: bool,
}
fn main() {
let p = Player { name: String::from("Ferris"), level: 7, online: true };
let json = serde_json::to_string(&p).unwrap();
println!("{json}");
}
{"name":"Ferris","level":7,"online":true}
Piece by piece:
use serde::Serialize;brings in the derive macro (it has the same name as the trait).#[derive(Serialize)]generatesimpl Serialize for Playerat compile time, the same way#[derive(Debug)]generatesDebug. You never write that impl by hand for ordinary structs.serde_json::to_string(&p)takes a reference (serializing only reads the value) and returnsResult<String, serde_json::Error>. Field names become JSON keys, in the order the fields are declared.- Why a
Result? Serialization can fail in rare cases, for example a map whose keys are tuples or structs, which can't become JSON object keys. For a plain struct like this it never fails, sounwrap(or better,expectwith a reason) is accepted practice. In functions that already return aResult, use?.
How Rust types map to JSON
| Rust | JSON |
|---|---|
String, &str, char |
string "..." |
| integers, floats | number |
bool |
true / false |
Vec<T>, arrays, slices, tuples |
array [...] |
Option<T> |
the value, or null for None |
structs, BTreeMap<String, T>, HashMap<String, T> |
object {...} |
Structs can contain other structs, as long as every type inside also implements Serialize. For humans, to_string_pretty adds newlines and two-space indentation:
use serde::Serialize;
#[derive(Serialize)]
struct Item {
sku: String,
qty: u32,
}
#[derive(Serialize)]
struct Order {
id: u64,
items: Vec<Item>,
note: Option<String>,
}
fn main() {
let order = Order {
id: 1001,
items: vec![Item { sku: String::from("CRAB-01"), qty: 2 }],
note: None,
};
println!("{}", serde_json::to_string_pretty(&order).expect("orders always serialize"));
}
{
"id": 1001,
"items": [
{
"sku": "CRAB-01",
"qty": 2
}
],
"note": null
}
Common mistakes
Forgetting the derive on an inner type. Order derives Serialize, but Item doesn't:
use serde::Serialize;
struct Item {
sku: String,
}
#[derive(Serialize)]
struct Order {
items: Vec<Item>,
}
fn main() {
let o = Order { items: vec![Item { sku: String::from("A") }] };
println!("{}", serde_json::to_string(&o).unwrap());
}
error[E0277]: the trait bound `Item: serde::Serialize` is not satisfied
The note under the error even suggests the fix: add #[derive(Serialize)] to Item. Two more:
- Forgetting
features = ["derive"]inCargo.toml: then#[derive(Serialize)]fails with "cannot find derive macroSerializein this scope". - Using
{:?}and calling it JSON. Debug output (Player { name: "Ferris", .. }) is for developers; other programs can't parse it. JSON comes fromserde_json.
Your task
Fill in the blanks so the program derives the trait and prints the ticket as compact JSON: {"id":42,"title":"Login button broken","open":true}.
Challenge 2
The starter code doesn't compile: one type is missing its derive. Fix it, then print the inventory with serde_json::to_string_pretty so the output is indented over several lines.
Challenge 3
Logs are often written as JSON Lines: one compact JSON object per line, so tools can process them line by line. Derive what Event needs and write fn to_json_lines(events: &[Event]) -> String that serializes each event with serde_json::to_string and joins them with \n (no newline after the last one; an empty slice gives an empty string).