JSON with serde_json
Parse and emit JSON with typed structs, dynamic Value, and streaming for large payloads.
Recipe
Quick-reference recipe card - copy-paste ready.
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Serialize, Deserialize)]
struct Event { kind: String, payload: Value }
let raw = r#"{"kind":"click","payload":{"x":1}}"#;
let ev: Event = serde_json::from_str(raw)?;
let dynamic = json!({ "ok": true, "count": ev.payload["x"] });
let out = serde_json::to_string(&dynamic)?;When to reach for this: HTTP APIs, config files, and logs overwhelmingly use JSON in Rust services.
Working Example
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u64,
email: String,
#[serde(default)]
tags: Vec<String>,
}
fn main() -> serde_json::Result<()> {
let u = User { id: 1, email: "a@b.co".into(), tags: vec![] };
let compact = serde_json::to_string(&u)?;
let pretty = serde_json::to_string_pretty(&u)?;
let parsed: User = serde_json::from_str(&compact)?;
assert_eq!(parsed.email, "a@b.co");
let mut v: Value = serde_json::from_str(r#"{"meta":{"version":1}}"#)?;
v["meta"]["source"] = json!("api");
assert_eq!(v["meta"]["version"], 1);
Ok(())
}What this demonstrates:
to_stringvsto_string_prettyfor wire vs debug output.from_strinto strongly typedUser.Valuefor partially structured or evolving JSON.json!macro for literal construction.
Deep Dive
How It Works
serde_jsonimplements Serde'sSerializerandDeserializerfor JSON.- Typed deserialization validates structure at parse time.
Valuedefers schema enforcement until you index fields.
Common APIs
| Function | Use |
|---|---|
from_str / from_slice | Parse to T |
to_string / to_vec | Serialize T |
from_value / to_value | Convert Value ↔ T |
json! | Build Value literals |
Rust Notes
// Fallible field access on Value
let n = v.get("count").and_then(|x| x.as_u64()).unwrap_or(0);Gotchas
- unwrap on parse - Untrusted JSON panics in production. Fix: Propagate
serde_json::Erroror map to 400/422. - f64 for money - Floating point rounding in JSON numbers. Fix: Store cents as integers or use decimal crates.
- Large DOM in memory -
Valueallocates entire document. Fix:serde_json::Deserializer::from_readerwith stream types. - UTF-8 loss - Invalid UTF-8 in strings fails deserialization. Fix: Validate upstream; use bytes only when required.
- Duplicate keys - Last key wins in
Value; typed structs error on duplicates by default. Fix: Know your producer behavior.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
simd-json | Maximum parse throughput | Portable builds without SIMD |
sonic-rs | High performance JSON | Ecosystem maturity matters most |
miniserde | Minimal dependency binary | Complex derive needs |
FAQs
Pretty vs compact?
Compact for wire; pretty for logs and fixtures only.
Parse without allocating String?
from_slice on &[u8] or from_reader on impl Read.
Unknown fields?
#[serde(deny_unknown_fields)] on structs for strict APIs.
Merge JSON objects?
Manipulate Value or deserialize to HashMap<String, Value>.
JSON Lines?
Read line by line with BufRead::lines and from_str per line.
Raw JSON embedding?
serde_json::raw::RawValue preserves sub-tree as bytes.
Axum integration?
Json<T> extractor uses serde_json under the hood.
NaN and Infinity?
Not standard JSON; serde_json rejects non-finite floats by default.
Benchmarking?
Use criterion with representative payload sizes.
Schema validation?
Add jsonschema crate after serde parse for contract checks.
Related
- serde Basics - Derive fundamentals
- Customizing Serialization - Field attributes
- Error Handling & Validation - Untrusted input
- Other Formats - TOML, YAML, bincode
- Request Validation - HTTP boundary
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+.