Skip to content

Serde

Serialization & FormatsSerializationRust

What it is

Serde is Rust's serialisation framework, converting between Rust data structures and formats such as JSON, YAML, TOML, MessagePack and many more via derive macros.

Derive Serialize and Deserialize on your types, then use a format crate to convert. Attributes control field names, defaults, skipping and flattening.

Installation

cargo add serde --features derive

Getting started

The smallest useful thing you can do with it, and what each part means.

Derive and round trip
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Book {
    id: u32,
    title: String,
    #[serde(default)]
    tags: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    subtitle: Option<String>,
}

let json = serde_json::to_string(&book)?;
let parsed: Book = serde_json::from_str(&json)?;
rename_all bridges Rust's snake_case and JSON's camelCase without annotating every field. `default` lets an absent array deserialise as empty rather than failing.
Enums as tagged unions
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Event {
    Click { x: i32, y: i32 },
    KeyPress { key: String },
    Close,
}

// {"type":"click","x":10,"y":20}

// Without a tag, Serde tries each variant in order — slower, and the
// error message on failure is much worse.
Internally tagged enums map cleanly onto the discriminated unions most APIs use, and give far better errors than untagged ones.

Advanced usage

Where the library earns its place over a simpler alternative.

Borrowed deserialisation and custom logic
// Borrow from the input instead of allocating a String per field.
#[derive(Deserialize)]
struct Row<'a> {
    #[serde(borrow)]
    name: &'a str,
}

// Custom conversion for a field.
fn ms_to_duration<'de, D>(d: D) -> Result<Duration, D::Error>
where D: serde::Deserializer<'de> {
    let ms = u64::deserialize(d)?;
    Ok(Duration::from_millis(ms))
}

#[derive(Deserialize)]
struct Config {
    #[serde(deserialize_with = "ms_to_duration")]
    timeout: Duration,
}
Zero-copy deserialisation avoids an allocation per string field, which is a large win when parsing many records — the borrowed data must outlive the struct.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

missing field `x` at line 1 column 20
The input lacks a required field. Add #[serde(default)] or make it Option<T> if it is genuinely optional.
the trait Serialize is not implemented
A nested type lacks the derive, or the derive feature is not enabled on the serde dependency.

Best practices

  • Enable the derive feature rather than implementing the traits by hand.
  • Use #[serde(deny_unknown_fields)] on configuration types so typos are errors, not silence.
  • Prefer tagged enums; untagged ones are slower and produce unusable error messages.
  • Use skip_serializing_if to keep None out of the output rather than emitting null.

Background

Why it exists, and what it was reacting to.

Written by David Tolnay and Erick Tryzelaar, Serde is arguably the most important library in the Rust ecosystem. Its trait-based design means a format crate and a data type written by strangers interoperate with no coordination.