Customizing Serialization
Shape JSON and config output with serde field and container attributes - no manual impls required for most cases.
Recipe
Quick-reference recipe card - copy-paste ready.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiUser {
user_id: u64,
#[serde(skip_serializing_if = "Option::is_none")]
nickname: Option<String>,
#[serde(default)]
active: bool,
}When to reach for this: External APIs use different naming, optional fields, or nested layouts than your Rust structs.
Working Example
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct User {
user_id: u64,
#[serde(skip_serializing_if = "Option::is_none")]
display_name: Option<String>,
#[serde(default)]
role: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct Envelope {
#[serde(flatten)]
user: User,
version: u32,
}
fn main() {
let u = User { user_id: 1, display_name: None, role: "member".into() };
let json = serde_json::to_string(&u).unwrap();
assert!(json.contains("userId"));
assert!(!json.contains("displayName"));
let env = Envelope { user: u, version: 2 };
let flat = serde_json::to_string(&env).unwrap();
assert!(flat.contains(r#""version":2"#));
assert!(flat.contains(r#""userId":1"#));
}What this demonstrates:
rename_allmaps Rust snake_case to camelCase JSON keys.skip_serializing_ifomitsNoneoptional fields from output.defaultfills missing keys on deserialize.flattenmerges nested struct fields into parent object.
Deep Dive
High-Value Attributes
| Attribute | Effect |
|---|---|
rename / rename_all | Key naming |
skip / skip_serializing | Omit fields |
default | Missing key → Default::default() |
flatten | Inline nested struct keys |
alias | Accept alternate key names on deserialize |
with | Custom serialize fn for field type |
Rust Notes
mod ts {
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(dt: &time::OffsetDateTime, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&dt.format(&time::format_description::well_known::Rfc3339).unwrap())
}
}
#[serde(with = "ts")]
created_at: time::OffsetDateTime,Gotchas
- flatten collisions - Two flattened structs with same field names. Fix: Rename fields or nest explicitly.
- skip without default - Deserializing old data missing new fields errors. Fix: Add
defaultorOption. - rename_all on enums - Affects variant names too - verify OpenAPI compatibility. Fix: Snapshot tests per format.
- skip_serializing_if on empty Vec - May hide intentional empty arrays. Fix: Use
skip_serializing_if = "Vec::is_empty"only when API omits empty lists. - with module path errors - Wrong signature on custom module. Fix: Copy template from serde docs for
with,serialize_with.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Separate DTO types | API and domain models diverge heavily | Small mapping overhead unacceptable |
serde_with crate | Common types like Duration | One-off with module suffices |
| Manual impl | Full control over wire shape | Attributes cover 90% of cases |
FAQs
skip vs skip_serializing?
skip removes field both directions; skip_serializing only affects output.
deserialize_with?
Custom fn when inbound format differs from outbound.
deny_unknown_fields?
Strict parsing - reject unexpected keys.
transparent?
Newtype wraps inner serialization without extra key.
bound attribute?
Relax generic bounds on derived impls for custom types.
tag on enum?
See Enums & Tagging page for tag, content attributes.
serialize_none?
Emit null vs omit key - control with skip_serializing_if.
multiple rename aliases?
alias = "oldName" accepts legacy keys when deserializing.
flatten + enum?
Supported with care - test round-trip thoroughly.
OpenAPI sync?
Generate from same structs with utoipa to avoid drift.
Related
- serde Basics - Derive intro
- Enums & Tagging - Enum attributes
- Custom (De)serialization - Manual impls
- JSON with serde_json - Wire format
- serde Best Practices - Compatibility rules
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.