iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…