Getting Started: serde + serde_json
Cargo.toml dependenciestoml
1[dependencies]2serde = { version = "1", features = ["derive"] }3serde_json = "1"Basic serialization and deserializationrust
1use serde::{Deserialize, Serialize};23#[derive(Debug, Serialize, Deserialize)]4struct User {5 name: String,6 age: u32,7 email: String,8}910fn main() -> Result<(), serde_json::Error> {11 // Deserialize JSON string -> Rust struct12 let json = r#"{"name":"Alice","age":30,"email":"[email protected]"}"#;13 let user: User = serde_json::from_str(json)?;14 println!("{:?}", user);1516 // Serialize Rust struct -> JSON string17 let json_output = serde_json::to_string_pretty(&user)?;18 println!("{}", json_output);1920 Ok(())21}Enums: Representing Polymorphic JSON
Tagged Enums (Default)
Externally tagged enumrust
1#[derive(Serialize, Deserialize)]2enum Shape {3 Circle { radius: f64 },4 Rectangle { width: f64, height: f64 },5}6// JSON: {"Circle":{"radius":5.0}}Internally Tagged and Untagged
Common enum representationsrust
1// Internally tagged: {"type":"circle","radius":5.0}2#[derive(Serialize, Deserialize)]3#[serde(tag = "type", rename_all = "lowercase")]4enum Shape {5 Circle { radius: f64 },6 Rectangle { width: f64, height: f64 },7}89// Adjacently tagged: {"t":"circle","c":{"radius":5.0}}10#[derive(Serialize, Deserialize)]11#[serde(tag = "t", content = "c")]12enum Message {13 Text(String),14 Image { url: String, width: u32 },15}1617// Untagged: tries each variant in order18#[derive(Serialize, Deserialize)]19#[serde(untagged)]20enum Value {21 Integer(i64),22 Float(f64),23 Text(String),24}Choosing an Enum Representation
Use internally tagged for API responses with a discriminator field (
"type"). Use untagged when consuming third-party JSON where you do not control the format. Avoid untagged for large enums as serde tries each variant sequentially.Dynamic JSON with serde_json::Value
Working with untyped JSONrust
1use serde_json::{json, Value};23fn main() -> Result<(), Box<dyn std::error::Error>> {4 // Parse into dynamic Value5 let data: Value = serde_json::from_str(r#"{"users":[{"name":"Alice"},{"name":"Bob"}]}"#)?;67 // Access nested fields8 let first_name = &data["users"][0]["name"];9 println!("First user: {}", first_name); // "Alice"1011 // Build JSON with the json! macro12 let response = json!({13 "status": "ok",14 "count": 42,15 "items": [1, 2, 3],16 "metadata": null17 });1819 // Modify values20 let mut config: Value = json!({"debug": false, "port": 8080});21 config["debug"] = json!(true);22 config["host"] = json!("0.0.0.0");2324 println!("{}", serde_json::to_string_pretty(&config)?);25 Ok(())26}Custom Serialize and Deserialize
Custom date serializationrust
1use chrono::{DateTime, Utc};2use serde::{self, Deserialize, Deserializer, Serializer};34const FORMAT: &str = "%Y-%m-%dT%H:%M:%SZ";56pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>7where S: Serializer {8 let s = date.format(FORMAT).to_string();9 serializer.serialize_str(&s)10}1112pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>13where D: Deserializer<'de> {14 let s = String::deserialize(deserializer)?;15 DateTime::parse_from_str(&s, FORMAT)16 .map(|dt| dt.with_timezone(&Utc))17 .map_err(serde::de::Error::custom)18}1920// Usage:21#[derive(Serialize, Deserialize)]22struct Event {23 name: String,24 #[serde(with = "self")] // use the custom module above25 timestamp: DateTime<Utc>,26}Zero-Copy Deserialization
Borrow string data directly from the input buffer to avoid allocations. Use &'a str instead of String:
Zero-copy with borrowed stringsrust
1#[derive(Deserialize)]2struct LogEntry<'a> {3 #[serde(borrow)]4 message: &'a str, // borrows from the input buffer5 #[serde(borrow)]6 service: &'a str,7 level: u8, // primitives are always copied (trivially cheap)8}910fn process_logs(input: &[u8]) -> Result<(), serde_json::Error> {11 // from_slice enables borrowing from the byte buffer12 let entry: LogEntry = serde_json::from_slice(input)?;13 println!("{}: {}", entry.service, entry.message);14 Ok(())15}Note
Zero-copy deserialization requires the input to outlive the deserialized struct. It works with
from_slice and from_str but not with from_reader (which buffers internally).High-Performance Parsing with simd-json
For maximum throughput, simd-json uses SIMD CPU instructions to parse JSON 2–4x faster than serde_json. It implements the serde Deserialize trait, making it a near drop-in replacement:
Using simd-json with serde structsrust
1use simd_json;2use serde::Deserialize;34#[derive(Deserialize)]5struct Record {6 id: u64,7 name: String,8 score: f64,9}1011fn parse_fast(input: &mut [u8]) -> Result<Record, simd_json::Error> {12 // simd-json requires a mutable slice (it modifies the buffer in place)13 simd_json::serde::from_slice(input)14}1516// For dynamic parsing without structs:17fn parse_dynamic(input: &mut [u8]) -> Result<simd_json::OwnedValue, simd_json::Error> {18 simd_json::to_owned_value(input)19}| Library | Throughput (approx) | Allocation | API |
|---|---|---|---|
| serde_json | ~300 MB/s | Standard | Idiomatic serde |
| simd-json | ~1.2 GB/s | Minimal (in-place) | serde-compatible (mutable buffer required) |
| serde_json from_slice | ~350 MB/s | Reduced (zero-copy possible) | Idiomatic serde with borrowing |
Streaming JSON with StreamDeserializer
Process JSON Lines (NDJSON) streamsrust
1use serde::Deserialize;2use std::io::BufReader;3use std::fs::File;45#[derive(Deserialize)]6struct LogLine {7 timestamp: String,8 level: String,9 message: String,10}1112fn process_log_file(path: &str) -> Result<(), Box<dyn std::error::Error>> {13 let file = File::open(path)?;14 let reader = BufReader::new(file);15 let stream = serde_json::Deserializer::from_reader(reader)16 .into_iter::<LogLine>();1718 let mut error_count = 0u64;19 for result in stream {20 let entry = result?;21 if entry.level == "error" {22 error_count += 1;23 }24 }25 println!("Total errors: {}", error_count);26 Ok(())27}Error Handling Patterns
Robust error handling with contextrust
1use serde::Deserialize;2use thiserror::Error;34#[derive(Error, Debug)]5enum AppError {6 #[error("Failed to parse JSON: {0}")]7 JsonParse(#[from] serde_json::Error),89 #[error("Invalid data: {field} — {reason}")]10 Validation { field: String, reason: String },11}1213#[derive(Deserialize)]14struct Config {15 port: u16,16 host: String,17 max_connections: u32,18}1920fn load_config(json: &str) -> Result<Config, AppError> {21 let config: Config = serde_json::from_str(json)?;2223 if config.port == 0 {24 return Err(AppError::Validation {25 field: "port".into(),26 reason: "must be non-zero".into(),27 });28 }29 Ok(config)30}3132// serde_json errors include line/column for debugging:33// "expected value at line 3 column 12"Try These Tools
Continue Learning
Frequently Asked Questions
What is serde in Rust?
serde (SERialize/DEserialize) is Rust's framework for converting data structures to and from various formats. serde_json is the JSON-specific implementation. Together, they are the most downloaded crates on crates.io, handling billions of JSON operations in production systems worldwide.
How do I parse JSON into a Rust struct?
Add serde and serde_json to your Cargo.toml, derive Serialize and Deserialize on your struct, then call serde_json::from_str(&json_string). The derive macro generates all the parsing code at compile time with zero runtime reflection.
What is the difference between serde_json::Value and typed deserialization?
serde_json::Value is a dynamic type that can hold any JSON value (like a HashMap/Vec tree). Typed deserialization maps JSON directly into your Rust structs with compile-time type safety. Use Value for unknown schemas; use structs when the shape is known.
What is zero-copy deserialization in serde?
Zero-copy deserialization borrows string data directly from the input buffer instead of allocating new String objects. You use &'a str instead of String in your structs and deserialize with serde_json::from_slice. This significantly reduces allocations for read-heavy workloads.
How fast is simd-json compared to serde_json?
simd-json uses SIMD CPU instructions to parse JSON and is typically 2-4x faster than serde_json for large payloads. It is a drop-in replacement that implements the serde Deserialize trait, so you can switch without changing your struct definitions.
How do I handle optional fields in JSON with serde?
Wrap the field type in Option<T> and add #[serde(default)] or #[serde(skip_serializing_if = "Option::is_none")]. Option::None maps to missing JSON fields, and you can use #[serde(default)] to provide fallback values for any type.