Every way to save and send structured data
serde with JSON, TOML, CSV or a binary format, attributes to match someone else's shape, custom and validated serialization, and reading data you don't trust.
Choose the format by who reads it next.
- A person edits it (a config file) → TOML.
- Another program or a web browser (an API, a message) → JSON.
- A spreadsheet or a data tool (rows of the same fields) → CSV.
- Only your own Rust programs, and size or speed matters (a cache, a network packet, a microcontroller) → a binary format such as bincode or postcard. Not human-readable, and harder to evolve.
Then:
- The data's shape doesn't match your struct (camelCase keys, a
missing field, an enum written as a
"type"tag) → serde attributes, not hand-written code. - A value must be checked as it's read (a port in range, a
non-empty name) →
#[serde(try_from = ...)]so an invalid value never becomes your type. - It comes from someone you don't trust → limit its size, reject unknown fields, and validate before you use it.
- You only need a single value as text →
DisplayandFromStrare enough; serde is for whole data structures.
The model
Section
The serde model
Derive Serialize and Deserialize once, then pick any format.
Example
serde_json::to_string(&Reading { zone: "herbs".into(), celsius: 21.5 })? gives {"zone":"herbs","celsius":21.5}.Formats
Section
JSON
Typed structs, or serde_json::Value when the shape isn't known.
Section
TOML and CSV
Config files people edit, and rows for spreadsheets.
Section
Binary formats
Smaller and faster, and what you give up for it.
Matching someone else's shape
Section
Attributes
rename_all, rename, default, skip, alias, and the four ways to tag an enum.
Example
#[serde(tag = "type")] writes Event::Alarm { zone } as {"type":"Alarm","zone":"herbs"} instead of {"Alarm":{"zone":"herbs"}}.serialize_with, try_from, and a type that can't hold a bad value.
Data you don't trust
Section
Untrusted input
Size limits, deny_unknown_fields, and validating before use.
Formal. Check untrusted data once, at the boundary, by converting it
into a type whose values are valid by construction (a newtype with a
private field and a fallible constructor or TryFrom impl, an enum
instead of a string, NonZeroU32 instead of u32). Code that receives
the type can rely on its invariant without checking again. With serde,
#[serde(try_from = "Raw")] or a custom Deserialize makes
deserialization itself perform the conversion, so invalid input becomes
a deserialization error.
In plain English. Instead of checking a value and then carrying it around as a plain string or number (and hoping everyone remembers it was checked), turn it into a special type that can only ever hold good values. The check happens once, and the type remembers it.
Example. struct Port(u16) with impl TryFrom<u16> for Port that
rejects 0: a function taking Port never has to ask whether it's zero.
Why this rule exists. Scattered checks get forgotten; a type that can't represent bad data makes the compiler enforce the check everywhere.
Turn raw data into a valid type once, at the edge, instead of checking it everywhere.
Section
Input validation and secrets
The same discipline for every request a service accepts.
Where it's used
Section
Configuration files
A TOML file layered under environment variables and flags.
Section
JSON in a web handler
The Json extractor deserializes the request body for you.