serde
serde is the serialization framework most Rust services build on. It separates data shape (your types) from wire format (JSON, MessagePack, TOML, and more) through Serialize and Deserialize traits.
Recipe
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u64,
email: String,
}
fn main() -> serde_json::Result<()> {
let u = User { id: 1, email: "ada@acme.com".into() };
let json = serde_json::to_string(&u)?;
let back: User = serde_json::from_str(&json)?;
assert_eq!(back.email, "ada@acme.com");
Ok(())
}When to reach for this:
- Any API boundary (HTTP, CLI output, config files)
- Database rows mapped to structs (with
sqlxorsea-orm) - Event payloads in queues or logs
Working Example
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Event {
#[serde(rename = "event_type")]
kind: String,
#[serde(default)]
retries: u32,
}
// Accepts {"event_type":"created"} without retries field
let e: Event = serde_json::from_str(r#"{"event_type":"created"}"#).unwrap();What this demonstrates:
- Derive macros cover 90% of structs and enums
- Field attributes control naming and defaults
- Same type serializes to JSON, YAML, or bincode by swapping crates
Deep Dive
How It Works
- Traits -
Serializewrites a type;Deserializereconstructs it. - Formats -
serde_json,serde_yaml,toml,bincodeare separate crates. - Derive -
#[derive(Serialize, Deserialize)]generates impls at compile time. - Custom -
#[serde(with = "module")]or manual impls for odd formats.
Cross-Link
For exhaustive coverage, see the dedicated serde basics section: custom serializers, enum tagging, and zero-copy patterns.
Gotchas
- Floating point in JSON - NaN and large integers may not round-trip. Fix: use strings or fixed-point types for money.
- Untagged enums - ambiguous when variants look alike. Fix: prefer
#[serde(tag = "type")]for APIs. - Large derives on hot paths - derive is fine; avoid
serde_json::Valueeverywhere. Fix: typed structs at boundaries. - Skipping
deny_unknown_fieldson public APIs - clients send typos silently. Fix: enable on request types. - chrono vs time crate - pick one ecosystem-wide. Fix: standardize in workspace
Cargo.toml.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
miniserde | Minimal binary size | You need full derive ergonomics |
Manual Display | Tiny fixed formats | Evolving schemas |
protobuf / capnp | gRPC, strict schemas | Quick internal JSON APIs |
FAQs
Do I need serde_json if I only use TOML config?
No. Add only the format crates you use. serde alone has no format.
Can I serialize borrowed data?
Yes with #[serde(borrow)] and Deserialize<'de> when lifetimes are explicit. Owned types are simpler for APIs.
How do I skip None fields in JSON?
Use #[serde(skip_serializing_if = "Option::is_none")] on Option fields.
Is serde slow?
Derive-generated code is fast. Bottlenecks are usually IO and allocation, not serde itself.
Related
- serde basics - full section
- JSON with serde_json - API payloads
- anyhow & thiserror - error types that serialize cleanly
- Essential Crates Best Practices - dependency hygiene
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.