Rust serde_json Guide 2026: Parsing, Serialization, and Error Handling
A practical guide to using serde_json in Rust — typed deserialization, the Value enum, error handling patterns, key attributes like flatten and rename, and how to recover from malformed JSON at the API boundary.
Have broken JSON right now? Fix it free in under 1 second — no signup.
Fix My JSON →Rust developers working with JSON APIs reach for serde_json in almost every project. It is fast, type-safe, and integrates naturally with the Serde ecosystem. But a surprising amount of the rough edges — type mismatches, camelCase friction, malformed upstream responses — are easy to handle once you know the patterns.
This guide covers everything from first parse to production-ready error recovery.
Setup
Add the dependencies to Cargo.toml:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
The derive feature enables the procedural macros (#[derive(Serialize, Deserialize)]) that generate all the boilerplate at compile time.
Typed Deserialization
The most common pattern: deserialize a JSON string directly into a typed Rust struct.
use serde::Deserialize;
use serde_json::Result;
#[derive(Deserialize, Debug)]
struct ApiResponse {
id: u32,
message: String,
success: bool,
}
fn main() -> Result<()> {
let json = r#"{"id": 7, "message": "ok", "success": true}"#;
let resp: ApiResponse = serde_json::from_str(json)?;
println!("{:?}", resp);
Ok(())
}
Three entry points cover most use cases:
| Function | Input | Use case |
|---|---|---|
serde_json::from_str | &str | String you already hold |
serde_json::from_slice | &[u8] | Raw bytes from a network read |
serde_json::from_reader | impl Read | Streaming from file or HTTP body |
Serialization
The mirror of deserialization: derive Serialize and call to_string or to_string_pretty.
use serde::Serialize;
#[derive(Serialize)]
struct Event {
name: String,
timestamp: u64,
}
let event = Event { name: "click".into(), timestamp: 1_700_000_000 };
let json = serde_json::to_string(&event)?;
// {"name":"click","timestamp":1700000000}
let pretty = serde_json::to_string_pretty(&event)?;
to_vec and to_writer round out the API for byte output and streaming.
The serde_json::Value Enum
When the JSON schema is unknown at compile time — a plugin system, a generic proxy, a feature-flag payload — use serde_json::Value:
use serde_json::Value;
let v: Value = serde_json::from_str(r#"{"count": 3, "items": ["a", "b", "c"]}"#)?;
// Index with [] — returns Value::Null for missing keys
let count = &v["count"]; // Value::Number(3)
let first = &v["items"][0]; // Value::String("a")
Value has six variants: Null, Bool, Number, String, Array(Vec<Value>), and Object(Map<String, Value>). The json! macro constructs values inline:
use serde_json::json;
let payload = json!({
"action": "repair",
"input": broken_string,
"options": { "strict": false }
});
You can deserialize partially into a typed struct and capture the remainder as Value using a flattened HashMap:
use std::collections::HashMap;
#[derive(Deserialize)]
struct Envelope {
id: u64,
#[serde(flatten)]
extra: HashMap<String, Value>,
}
Key Serde Attributes
Attributes annotate structs and fields to control how JSON maps to Rust names and vice versa. The most useful ones:
rename and rename_all
APIs typically use camelCase; Rust conventions prefer snake_case. Bridge the gap at the struct level:
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct User {
user_id: u64, // maps from "userId"
display_name: String, // maps from "displayName"
}
Override an individual field with #[serde(rename = "...")], which takes precedence over rename_all.
default
Fill in missing fields with the type's Default implementation instead of failing:
#[derive(Deserialize)]
struct Config {
retries: u32,
#[serde(default)]
verbose: bool, // absent → false
}
Provide a custom default with #[serde(default = "fn_name")].
skip_serializing_if
Omit Option fields or empty collections from JSON output:
#[derive(Serialize)]
struct Record {
id: u64,
#[serde(skip_serializing_if = "Option::is_none")]
comment: Option<String>,
}
deny_unknown_fields
Reject unknown fields instead of silently ignoring them — useful in strict validation contexts. Note that deny_unknown_fields is incompatible with flatten.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictSchema {
field_a: String,
field_b: u32,
}
flatten
Inline a nested struct's fields into the parent JSON object:
#[derive(Deserialize)]
struct Pagination {
page: u32,
per_page: u32,
}
#[derive(Deserialize)]
struct ListResponse {
data: Vec<String>,
#[serde(flatten)]
pagination: Pagination,
}
// Parses: {"data": ["a"], "page": 2, "per_page": 20}
Error Handling
serde_json::Error exposes classification via .classify() and location via .line() / .column():
use serde_json::error::Category;
match serde_json::from_str::<MyStruct>(raw) {
Ok(v) => process(v),
Err(e) => {
eprintln!("parse error at {}:{} — {}", e.line(), e.column(), e);
match e.classify() {
Category::Syntax => eprintln!("malformed JSON"),
Category::Data => eprintln!("type mismatch or missing field"),
Category::Eof => eprintln!("truncated input"),
Category::Io => eprintln!("I/O error"),
}
}
}
The Category::Eof case is common with HTTP clients that read partial responses due to connection timeouts or misconfigured content-length headers.
Handling Malformed API Responses
Real-world JSON from third-party APIs breaks in predictable ways: single quotes instead of double quotes, trailing commas, unquoted keys, truncated payloads. serde_json is strict by design — it will reject all of these.
For production pipelines, the pattern is: attempt strict parse → on failure, run a repair pass → reattempt parse.
The AI JSON Medic repair tool handles the most common LLM and API JSON breakage patterns: mismatched brackets, trailing commas, control characters in string values, and partially streamed output. Piping a repaired string back through serde_json::from_str restores the full typed deserialization flow.
If you're processing API data programmatically, you can also use the JSON validator to inspect what a specific JSON string's schema violation is before writing a deserializer for it.
A pragmatic recovery function:
/// Attempt strict parse; fall back to Value for inspection.
fn parse_or_inspect(raw: &str) -> Result<MyStruct, serde_json::Error> {
serde_json::from_str::<MyStruct>(raw).or_else(|strict_err| {
// Parse as Value to see the structure even if types don't match
match serde_json::from_str::<Value>(raw) {
Ok(v) => {
eprintln!("structure valid, type mismatch: {} — got: {}", strict_err, v);
Err(strict_err)
}
Err(_) => {
eprintln!("invalid JSON syntax: {}", strict_err);
Err(strict_err)
}
}
})
}
Enums in serde_json
Serde's enum representation gives you control over how tagged unions appear in JSON. The four representations:
// External tagging (default): {"Variant": {...}}
// Internal tagging: {"type": "Variant", ...}
// Adjacent tagging: {"t": "Variant", "c": {...}}
// Untagged: {field matching decides the variant}
#[derive(Deserialize)]
#[serde(tag = "type")] // internal tagging
enum Event {
Click { x: f64, y: f64 },
Keypress { key: String },
}
// Parses: {"type": "Click", "x": 1.5, "y": 2.0}
Internal tagging is the most common for API event streams. Untagged (#[serde(untagged)]) is useful when you accept either a string or a structured object for a field.
Performance Considerations
serde_json is fast for most workloads. For throughput-critical paths parsing megabyte-scale JSON:
simd-json: Uses SIMD CPU instructions, 2–4× faster than serde_json on supported platforms. API-compatible via thesimd-jsoncrate'sserde_implfeature.from_slicevsfrom_str: Preferfrom_slicewhen you haveVec<u8>from a network read — avoids a UTF-8 validation copy.- Streaming with
StreamDeserializer: Useserde_json::Deserializer::from_reader(...).into_iter::<T>()to process large JSONL files record-by-record without loading the full file.
Quick Reference
// Parse
let val: MyType = serde_json::from_str(s)?;
let val: MyType = serde_json::from_slice(bytes)?;
let val: Value = serde_json::from_str(s)?; // untyped
// Build
let s: String = serde_json::to_string(&val)?;
let b: Vec<u8> = serde_json::to_vec(&val)?;
let p: String = serde_json::to_string_pretty(&val)?;
let v: Value = json!({ "key": "value", "n": 42 });
// Error info
let err: serde_json::Error = ...;
err.line(); err.column(); err.classify();
// Null-safe indexing on Value
let x = &v["key"]["nested"]; // Value::Null if absent
if let Some(s) = x.as_str() { ... }
FAQ
What is serde_json in Rust?serde_json is the standard JSON library for Rust, built on top of the Serde framework. It provides typed deserialization into Rust structs, serialization to JSON strings or bytes, and the serde_json::Value enum for handling arbitrary JSON at runtime.
Use #[serde(deny_unknown_fields)] to reject unknown fields, or omit it (the default) to silently ignore them. For partial unknown data, use #[serde(flatten)] with a HashMap<String, Value> to capture leftover fields.
Common causes: type mismatches (JSON number received where string expected), null values on non-Option fields, snake_case vs camelCase naming divergence, and truncated or malformed JSON from the network layer. Use serde_json::from_str and inspect the Error for line/column info.
Use #[serde(rename = "fieldName")] on individual fields or #[serde(rename_all = "camelCase")] on the struct for bulk renaming. Field-level rename takes precedence over rename_all.
serde_json::Value is an untyped enum representing any valid JSON value. Use it when the JSON schema is unknown at compile time, when building generic utilities, or when you need to inspect a partial structure before full deserialization.
Working across Rust and TypeScript in the same project?
If you're sharing JSON schemas between a Rust backend and a TypeScript frontend, use AI JSONMedic's JSON to TypeScript converter to generate TypeScript interfaces from your Rust-serialized JSON payloads. It's a fast way to keep both sides in sync without hand-writing duplicate type definitions.
Still dealing with broken JSON?
Paste it in and get it fixed in under 1 second — free, no signup, no install. Works with ChatGPT, Claude, n8n, and any AI output.
Fix My JSON Free →Related Articles