JSON in Rust means serde
Rust doesn't have JSON in its standard library — and that's fine, because the ecosystem standardized on serde (serialize/deserialize) and serde_json so thoroughly they're effectively the default. serde's trick is compile-time code generation: you annotate a struct, and the compiler writes fast, correct parsing and serialization for you. The result is JSON handling that's both ergonomic and impossible to get subtly wrong at runtime the way dynamic languages allow.
# Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"Typed parsing: derive and go
The idiomatic path is a struct with #[derive(Serialize, Deserialize)]. serde maps JSON fields to struct fields by name:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
id: u64,
name: String,
email: String,
active: bool,
}
fn main() -> serde_json::Result<()> {
let data = r#"{"id":1,"name":"Ada","email":"ada@example.com","active":true}"#;
let user: User = serde_json::from_str(data)?; // parse
println!("{}", user.name);
let back = serde_json::to_string(&user)?; // serialize
println!("{}", back);
Ok(())
}If the JSON is missing id or id is a string, from_str returns an Err — you handle the failure explicitly instead of discovering undefined three functions later. That's the safety Rust buys you.
Untyped parsing: serde_json::Value
When the shape is unknown or dynamic, parse into serde_json::Value — an enum that can hold any JSON node — and navigate it:
use serde_json::Value;
let v: Value = serde_json::from_str(r#"{"user":{"name":"Ada"}}"#)?;
if let Some(name) = v["user"]["name"].as_str() {
println!("{name}");
}Use Value for config you probe dynamically or truly heterogeneous data; prefer typed structs everywhere else, because they document the shape and catch mismatches at parse time.
The attributes you'll actually use
serde's power is in field attributes that bridge idiomatic Rust and real-world JSON:
- Rename between conventions. JSON APIs use camelCase; Rust uses snake_case:
- ```rust
- #[derive(Deserialize)]
- #[serde(rename_all = "camelCase")]
- struct Account { user_id: u64, created_at: String } // reads "userId", "createdAt"
`- Optional fields map to
Option<T>— a missing ornullvalue becomesNoneinstead of an error. - Defaults with
#[serde(default)]fill absent fields. - Skip serializing empties with
#[serde(skip_serializing_if = "Option::is_none")]to keep output clean.
#[derive(Serialize, Deserialize)]
struct Profile {
name: String,
#[serde(default)]
bio: String,
#[serde(skip_serializing_if = "Option::is_none")]
avatar: Option<String>,
}From a JSON sample to Rust structs
Hand-writing structs for a large API response is tedious and error-prone. Speed it up:
- Format the API sample so the shape is clear and you can spot optional fields.
- Generate the structs automatically with JSON to Rust — it emits serde-annotated structs, including nested types.
- Validate real payloads against the shape when the API is one you don't control, so a silent field change surfaces in tests.
For the same ideas in other languages, see JSON in Python, JSON to Go structs, and JSON to TypeScript.