serde
serde is Rusts serialisation framework: a single Serialize/Deserialize trait you derive on your types, plus format-specific crates (serde_json, serde_yaml, toml, bincode). The pattern that scales: model your domain in Rust types, derive Serialize/Deserialize once, and every format is one line away. Strong typing turns API payloads into compile-time checked structures.
Derive, customise, validate, and handle unknown fields
EXAMPLE
// Cargo.toml
// [dependencies]
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
// chrono = { version = "0.4", features = ["serde"] }
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
// ============================================================
// 1) Minimal type — derive does everything
// ============================================================
#[derive(Debug, Serialize, Deserialize)]
pub struct Money {
pub amount: i64,
pub currency: String,
}
// ============================================================
// 2) Field rename and snake_case <-> camelCase mapping
// ============================================================
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Order {
pub id: String,
pub customer: String,
pub total: Money,
#[serde(rename = "created_at")]
pub created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
// ============================================================
// 3) Tagged union for polymorphic payloads
// ============================================================
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Event {
OrderPaid { order_id: String },
OrderShipped { order_id: String, tracking: String },
}
// JSON: { "type": "order_paid", "order_id": "o1" }
// { "type": "order_shipped", "order_id": "o1", "tracking": "AU-9F3" }
// ============================================================
// 4) Default values + deny unknown fields (defends against API drift)
// ============================================================
fn default_status() -> String { "new".to_string() }
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OrderInput {
pub customer: String,
pub total_cents: i64,
#[serde(default = "default_status")]
pub status: String,
}
// ============================================================
// 5) Custom serialisation for a single field
// ============================================================
mod cents_to_dollars {
use serde::{de::Deserializer, ser::Serializer, Deserialize};
pub fn serialize<S: Serializer>(cents: &i64, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&format!("{:.2}", *cents as f64 / 100.0))
}
pub fn deserialize<>(d: D) -> Result<i64, D::Error> where D: Deserializer<> {
let s = String::deserialize(d)?;
let f: f64 = s.parse().map_err(serde::de::Error::custom)?;
Ok((f * 100.0) as i64)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Receipt {
pub id: String,
#[serde(with = "cents_to_dollars")]
pub total_cents: i64,
}
// ============================================================
// 6) Validation after deserialise — TryFrom pattern
// ============================================================
#[derive(Debug, Deserialize)]
struct EmailInput { email: String }
#[derive(Debug)]
struct Email(String);
impl TryFrom<EmailInput> for Email {
type Error = String;
fn try_from(v: EmailInput) -> Result<Self, Self::Error> {
if v.email.contains('@') { Ok(Email(v.email.to_lowercase())) }
else { Err(format!("invalid email: {}", v.email)) }
}
}
// ============================================================
// 7) Round-trip
// ============================================================
fn main() -> serde_json::Result<()> {
let o = Order {
id: "o1".into(),
customer: "alice".into(),
total: Money { amount: 4995, currency: "AUD".into() },
created_at: Utc::now(),
notes: None,
};
// Serialize
let s = serde_json::to_string_pretty(&o)?;
println!("{s}");
// Deserialize
let back: Order = serde_json::from_str(&s)?;
println!("{back:?}");
Ok(())
}
Why it matters
Use #[serde(deny_unknown_fields)] on inputs you accept from clients. Without it, typos in client payloads are silently dropped and the bug surfaces as a missing field on the database row two days later — with it, the client gets a 400 the moment they mis-spell `totalCents` as `total_cnets`.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct User { name: String, age: u32 }
let u = User { name: "Ada".into(), age: 36 };
let json = serde_json::to_string(&u).unwrap();
Try it Yourself »
Discussion
Loading…