What is Serde?
Serde (Serialize + Deserialize) is Rust's framework for efficiently and generically serializing and deserializing data structures. It's the most downloaded crate on crates.io and supports JSON, TOML, YAML, MessagePack, and many more formats.
Basic Usage with Derive Macros
// Cargo.toml:
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u64,
name: String,
email: String,
active: bool,
}
fn main() -> serde_json::Result<()> {
// Serialize to JSON
let user = User {
id: 1,
name: "Alice".into(),
email: "alice@example.com".into(),
active: true,
};
let json_string = serde_json::to_string(&user)?;
println!("JSON: {json_string}");
let pretty = serde_json::to_string_pretty(&user)?;
println!("Pretty:\n{pretty}");
// Deserialize from JSON
let json = r#"{"id":2,"name":"Bob","email":"bob@test.com","active":false}"#;
let bob: User = serde_json::from_str(json)?;
println!("Deserialized: {:?}", bob);
// Work with dynamic JSON
let value: serde_json::Value = serde_json::from_str(json)?;
println!("Name: {}", value["name"]);
// Create JSON dynamically
let dynamic = serde_json::json!({
"message": "hello",
"count": 42,
"items": [1, 2, 3]
});
println!("{}", dynamic);
Ok(())
}
Field Attributes
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] // field_name -> fieldName
struct ApiResponse {
user_id: u64,
display_name: String,
#[serde(rename = "e_mail")] // Custom field name
email: String,
#[serde(skip_serializing_if = "Option::is_none")]
phone: Option,
#[serde(default)] // Use Default::default() if missing
verified: bool,
#[serde(skip)] // Never serialize/deserialize
internal_cache: String,
#[serde(alias = "created")] // Accept alternative name
created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct Wrapper {
version: String,
#[serde(flatten)] // Flatten nested struct
data: UserData,
}
#[derive(Debug, Serialize, Deserialize)]
struct UserData {
name: String,
age: u32,
}
// Serializes as: {"version":"1","name":"Alice","age":30}
// Instead of: {"version":"1","data":{"name":"Alice","age":30}}
Enum Representations
use serde::{Serialize, Deserialize};
// Externally tagged (default): {"Circle": {"radius": 5}}
#[derive(Serialize, Deserialize, Debug)]
enum ShapeDefault {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
// Internally tagged: {"type": "circle", "radius": 5}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", rename_all = "lowercase")]
enum ShapeInternal {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
// Adjacently tagged: {"t": "circle", "c": {"radius": 5}}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "t", content = "c")]
enum ShapeAdjacent {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
// Untagged: {"radius": 5} (tries each variant)
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
enum ShapeUntagged {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
fn main() {
let shape = ShapeInternal::Circle { radius: 5.0 };
let json = serde_json::to_string(&shape).unwrap();
println!("{json}"); // {"type":"circle","radius":5.0}
}
Working with APIs
use serde::{Serialize, Deserialize};
// Real-world API response handling
#[derive(Debug, Deserialize)]
struct GitHubRepo {
name: String,
full_name: String,
description: Option,
#[serde(rename = "stargazers_count")]
stars: u64,
language: Option,
#[serde(rename = "html_url")]
url: String,
}
#[derive(Debug, Deserialize)]
struct PaginatedResponse {
data: Vec,
total: u64,
page: u32,
per_page: u32,
}
// Custom deserialization for dates
#[derive(Debug, Serialize, Deserialize)]
struct Event {
name: String,
#[serde(with = "date_format")]
date: chrono::NaiveDate,
}
mod date_format {
use chrono::NaiveDate;
use serde::{self, Deserialize, Deserializer, Serializer};
const FORMAT: &str = "%Y-%m-%d";
pub fn serialize(date: &NaiveDate, serializer: S) -> Result
where S: Serializer {
serializer.serialize_str(&date.format(FORMAT).to_string())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result
where D: Deserializer<'de> {
let s = String::deserialize(deserializer)?;
NaiveDate::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)
}
}
// TOML example
// toml = "0.8"
// let config: Config = toml::from_str(&std::fs::read_to_string("Config.toml")?)?;
Key Takeaways
- ✅ Serde derive macros make serialization effortless with
#[derive(Serialize, Deserialize)] - ✅ Field attributes control renaming, skipping, defaults, and flattening
- ✅ Enum representations (tagged, untagged, etc.) handle complex API schemas
- ✅
serde_json::Valueenables dynamic JSON when types are unknown - ✅ Serde works with JSON, TOML, YAML, and dozens of other formats