Serde: The Foundation
Serde is Rust’s serialization framework — zero-cost abstractions with compile-time code generation.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
name: String,
age: u32,
#[serde(default)] // Optional: Option<T> already deserializes missing fields as None
email: Option<String>,
#[serde(rename = "createdAt")]
created_at: chrono::DateTime<chrono::Utc>,
}
// Serialize
let user = User {
name: "Alice".to_string(),
age: 30,
email: Some("alice@example.com".to_string()),
created_at: chrono::Utc::now(),
};
let json = serde_json::to_string_pretty(&user)?;
// Deserialize
let parsed: User = serde_json::from_str(&json)?;
Field Attributes
Control serialization behavior at the field level:
use std::collections::HashMap;
#[derive(Serialize, Deserialize)]
struct Config {
// Rename for JSON compatibility
#[serde(rename = "databaseUrl")]
database_url: String,
// Skip if None
#[serde(skip_serializing_if = "Option::is_none")]
optional_field: Option<String>,
// Default value if missing
#[serde(default = "default_timeout")]
timeout: u64,
// Always skip
#[serde(skip)]
internal_state: Vec<u8>,
// Flatten nested struct
#[serde(flatten)]
metadata: HashMap<String, serde_json::Value>,
}
fn default_timeout() -> u64 {
30
}
Custom Serialization
Implement Serialize and Deserialize for full control:
use serde::{Deserializer, Serializer};
use serde::de::{self, Visitor};
use std::fmt;
struct CustomDate(chrono::NaiveDate);
impl Serialize for CustomDate {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = self.0.format("%Y-%m-%d").to_string();
serializer.serialize_str(&s)
}
}
impl<'de> Deserialize<'de> for CustomDate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct DateVisitor;
impl<'de> Visitor<'de> for DateVisitor {
type Value = CustomDate;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a date string in YYYY-MM-DD format")
}
fn visit_str<E>(self, value: &str) -> Result<CustomDate, E>
where
E: de::Error,
{
chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d")
.map(CustomDate)
.map_err(de::Error::custom)
}
}
deserializer.deserialize_str(DateVisitor)
}
}
Enum Serialization
Handle Rust enums in JSON:
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
// {"type": "Circle", "radius": 5.0}
let circle = Shape::Circle { radius: 5.0 };
let json = serde_json::to_string(&circle)?;
// Externally tagged (default)
#[derive(Serialize, Deserialize)]
enum Color {
Red,
Green,
Blue,
}
// "Red"
// Adjacently tagged
#[derive(Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
enum Value {
Int(i64),
Text(String),
}
// {"t": "Int", "c": 42}
Error Handling
Robust JSON parsing with proper error types:
use thiserror::Error;
#[derive(Error, Debug)]
enum JsonError {
#[error("JSON parsing error: {0}")]
Parse(#[from] serde_json::Error),
#[error("Validation error: {message}")]
Validation { message: String },
#[error("Missing field: {field}")]
MissingField { field: String },
}
fn parse_config(json: &str) -> Result<Config, JsonError> {
let config: Config = serde_json::from_str(json)?;
if config.database_url.is_empty() {
return Err(JsonError::MissingField {
field: "databaseUrl".to_string(),
});
}
Ok(config)
}
Performance Optimization
Use simd-json for SIMD-accelerated parsing
use simd_json::prelude::*;
let mut data = json_string.to_owned();
let parsed: User = simd_json::from_str(&mut data)?;
// 2-3x faster than serde_json for large payloads
Avoid allocations with borrow
#[derive(Deserialize)]
struct User<'a> {
name: &'a str, // Borrows from input string
age: u32,
}
let user: User = serde_json::from_str(&json)?;
// No allocation for string fields
Stream processing for large files
use serde_json::Deserializer;
let reader = std::fs::File::open("large.json")?;
let stream = Deserializer::from_reader(reader).into_iter::<User>();
for result in stream {
let user = result?;
process_user(user);
}
Working with Dynamic JSON
For unknown structures, use serde_json::Value:
use serde_json::{Value, json};
// Parse into dynamic Value
let data: Value = serde_json::from_str(json_string)?;
// Access fields
let name = data["name"].as_str().unwrap_or("unknown");
let age = data["age"].as_i64().unwrap_or(0);
// Build JSON with the json! macro
let user = json!({
"name": "Alice",
"age": 30,
"tags": ["rust", "json"]
});
// Convert Value back to string
let output = serde_json::to_string_pretty(&user)?;
Untagged Enums
When JSON doesn’t have a type discriminator:
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Value {
Int(i64),
Text(String),
Bool(bool),
}
// Tries each variant in order: Int, then Text, then Bool
Related Resources
- JSON Best Practices — Universal guidelines for all languages
- JSON Schema Validation — Validate your JSON structures
- JSON Security — Protect against common vulnerabilities
- JSON Formatter — Format and validate your JSON online
- JSON Schema Validator — Test your schemas