Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

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 → Display and FromStr are enough; serde is for whole data structures.

The model

Section

The serde model

Derive Serialize and Deserialize once, then pick any format.

Turning a value into text or bytes to store or send it, and back again.

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.

How an enum's variant name is recorded in serialized data.

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.

The same discipline for every request a service accepts.

Where it's used

A TOML file layered under environment variables and flags.

The Json extractor deserializes the request body for you.