Serde Derive Basics
Serialization Is a Contract Boundary
In production systems, serialization is not just about converting structs to JSON. It defines your API contract with clients, databases, queues, and other services. Once exposed, that contract becomes hard to change. Serde is the de facto standard library for serialization in Rust and must be used deliberately.
Adding Serde to Your Project
# Cargo.toml
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
The derive feature allows automatic implementation of Serialize and Deserialize traits.
Basic Derive Example
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct User {
id: u64,
email: String,
}
fn main() {
let user = User {
id: 1,
email: "a@example.com".to_string(),
};
let json = serde_json::to_string(&user).unwrap();
println!("{}", json);
}
This automatically maps struct fields to JSON keys with the same names.
Deserializing JSON
let input = r#"{"id":1,"email":"a@example.com"}"#;
let user: User = serde_json::from_str(input).unwrap();
println!("{}", user.email);
Production note: never unwrap in real request handlers. Always return proper error responses.
Renaming Fields for API Stability
Field names in Rust do not always match external API conventions. Use attributes to control mapping.
#[derive(Serialize, Deserialize)]
struct User {
#[serde(rename = "user_id")]
id: u64,
#[serde(rename = "email_address")]
email: String,
}
This keeps internal naming clean while preserving external contract stability.
Optional Fields
Optional JSON fields map naturally to Option types.
#[derive(Serialize, Deserialize)]
struct CreateUser {
email: String,
nickname: Option,
}
If nickname is missing, it becomes None.
Skipping Fields
Sometimes you do not want to expose internal fields.
#[derive(Serialize, Deserialize)]
struct User {
id: u64,
email: String,
#[serde(skip_serializing)]
internal_flag: bool,
}
Production rule: never serialize secrets or internal flags unintentionally.
Default Values
When deserializing from older clients, fields may be missing. Use default values to preserve backward compatibility.
#[derive(Serialize, Deserialize)]
struct Config {
#[serde(default)]
retries: u32,
}
This prevents breaking changes when introducing new fields.
Strict vs Lenient Deserialization
By default, unknown fields are ignored. You can enforce stricter validation.
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictUser {
id: u64,
email: String,
}
Production rule: use deny_unknown_fields when strict API validation is required.
Error Handling During Deserialization
Never unwrap JSON parsing in production. Convert errors into structured responses.
fn parse_user(input: &str) -> Result{ serde_json::from_str(input) .map_err(|e| format!("invalid JSON payload: {}", e)) }
Attach context, but avoid logging raw request bodies if they may contain sensitive data.
Versioning Considerations
API contracts evolve. Backward compatibility strategies include:
- Adding optional fields
- Using default values
- Avoiding renaming existing fields without versioning
- Introducing new endpoint versions instead of breaking contracts
Serde makes it easy to maintain backward compatibility, but design discipline is still required.
Production Pitfalls
- Unwrapping JSON parsing errors
- Accidentally serializing secrets
- Changing field names without versioning
- Using overly permissive deserialization without validation
Production Checklist
- All external structs derive Serialize and Deserialize intentionally
- Field renaming explicit when API differs from internal naming
- Optional fields use Option
- Defaults applied for backward compatibility
- No secrets serialized
- Deserialization errors mapped to structured responses
Serde is more than a convenience tool. It defines the boundary between your Rust service and the outside world. Treat it as part of your production contract.