ExamplesBy LevelBy TopicLearning Paths
761 Fundamental

761-custom-serialize-logic — Custom Serialize Logic

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "761-custom-serialize-logic — Custom Serialize Logic" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. `#[derive(Serialize)]` handles the common case, but sometimes you need custom serialization: dates formatted as ISO strings rather than struct fields, passwords skipped entirely, amounts rounded before serialization, or opaque identifiers encoded as base64. Key difference from OCaml: 1. **Serde attributes**: Rust's `#[serde(skip)]`, `#[serde(rename = "...")]`, `#[serde(with = "...")]` provide common custom behaviors without full manual implementation; OCaml's equivalents are `[@yojson.key]`, `[@sexp.opaque]`.

Tutorial

The Problem

#[derive(Serialize)] handles the common case, but sometimes you need custom serialization: dates formatted as ISO strings rather than struct fields, passwords skipped entirely, amounts rounded before serialization, or opaque identifiers encoded as base64. Custom serialization implements Serialize (or serde::Serialize in production) manually, giving complete control over the wire format.

🎯 Learning Outcomes

  • • Implement custom serialization for a Date type as ISO 8601 string vs. compact integer
  • • Serialize Option fields as null/value with custom null representation
  • • Skip sensitive fields (password_hash) during serialization
  • • Implement custom deserialization that parses ISO strings back to structured Date values
  • • See how custom serialization enables format versioning and data migration
  • Code Example

    impl Date {
        pub fn to_iso_string(&self) -> String {
            format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
        }
        
        pub fn to_compact(&self) -> u32 {
            self.year as u32 * 10000 + self.month as u32 * 100 + self.day as u32
        }
    }

    Key Differences

  • Serde attributes: Rust's #[serde(skip)], #[serde(rename = "...")], #[serde(with = "...")] provide common custom behaviors without full manual implementation; OCaml's equivalents are [@yojson.key], [@sexp.opaque].
  • Format independence: Rust's custom Serialize impl works across all serde formats; OCaml's custom functions are typically format-specific.
  • with module: Rust's #[serde(with = "timestamp_seconds")] delegates to a module with serialize/deserialize functions; OCaml has no direct equivalent.
  • Versioning: Custom serialization enables format evolution (V1 → V2 migration); both languages use this for long-lived protocols.
  • OCaml Approach

    OCaml's ppx_sexp_conv allows custom sexp_of_t implementations that override the generated one. For JSON, ppx_yojson_conv supports [@yojson.option] and [@yojson.key "name"] attributes. Completely custom serialization replaces the generated function with a hand-written one in the same module. Bin_prot similarly allows custom bin_write_t/bin_read_t implementations.

    Full Source

    #![allow(clippy::all)]
    //! # Custom Serialize Logic
    //!
    //! When you need special serialization behavior.
    
    use std::fmt::Write;
    
    /// Date with custom serialization format
    #[derive(Debug, PartialEq, Clone)]
    pub struct Date {
        pub year: u16,
        pub month: u8,
        pub day: u8,
    }
    
    impl Date {
        pub fn new(year: u16, month: u8, day: u8) -> Self {
            Date { year, month, day }
        }
    
        /// Serialize as ISO 8601 string
        pub fn to_iso_string(&self) -> String {
            format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
        }
    
        /// Serialize as compact integer (YYYYMMDD)
        pub fn to_compact(&self) -> u32 {
            self.year as u32 * 10000 + self.month as u32 * 100 + self.day as u32
        }
    
        /// Parse from ISO string
        pub fn from_iso_string(s: &str) -> Option<Self> {
            let parts: Vec<&str> = s.split('-').collect();
            if parts.len() != 3 {
                return None;
            }
            Some(Date {
                year: parts[0].parse().ok()?,
                month: parts[1].parse().ok()?,
                day: parts[2].parse().ok()?,
            })
        }
    
        /// Parse from compact integer
        pub fn from_compact(n: u32) -> Self {
            Date {
                year: (n / 10000) as u16,
                month: ((n / 100) % 100) as u8,
                day: (n % 100) as u8,
            }
        }
    }
    
    /// Money with custom decimal serialization
    #[derive(Debug, PartialEq, Clone)]
    pub struct Money {
        /// Amount in cents
        cents: i64,
        currency: String,
    }
    
    impl Money {
        pub fn new(cents: i64, currency: &str) -> Self {
            Money {
                cents,
                currency: currency.to_string(),
            }
        }
    
        pub fn from_dollars(dollars: f64, currency: &str) -> Self {
            Money {
                cents: (dollars * 100.0).round() as i64,
                currency: currency.to_string(),
            }
        }
    
        pub fn to_display(&self) -> String {
            let dollars = self.cents / 100;
            let cents = (self.cents % 100).abs();
            if self.cents < 0 {
                format!("-{}.{:02} {}", dollars.abs(), cents, self.currency)
            } else {
                format!("{}.{:02} {}", dollars, cents, self.currency)
            }
        }
    
        /// Serialize as JSON object
        pub fn to_json(&self) -> String {
            format!(
                r#"{{"cents": {}, "currency": "{}"}}"#,
                self.cents, self.currency
            )
        }
    }
    
    /// Secret value that redacts in serialization
    #[derive(Clone)]
    pub struct Secret<T> {
        value: T,
    }
    
    impl<T> Secret<T> {
        pub fn new(value: T) -> Self {
            Secret { value }
        }
    
        pub fn expose(&self) -> &T {
            &self.value
        }
    }
    
    impl<T> std::fmt::Debug for Secret<T> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "Secret([REDACTED])")
        }
    }
    
    impl<T> std::fmt::Display for Secret<T> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "[REDACTED]")
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_date_iso() {
            let date = Date::new(2024, 3, 15);
            assert_eq!(date.to_iso_string(), "2024-03-15");
        }
    
        #[test]
        fn test_date_compact() {
            let date = Date::new(2024, 3, 15);
            assert_eq!(date.to_compact(), 20240315);
        }
    
        #[test]
        fn test_date_roundtrip_iso() {
            let original = Date::new(2024, 12, 25);
            let s = original.to_iso_string();
            let parsed = Date::from_iso_string(&s).unwrap();
            assert_eq!(original, parsed);
        }
    
        #[test]
        fn test_date_roundtrip_compact() {
            let original = Date::new(2024, 1, 1);
            let n = original.to_compact();
            let parsed = Date::from_compact(n);
            assert_eq!(original, parsed);
        }
    
        #[test]
        fn test_money_display() {
            let m = Money::new(1234, "USD");
            assert_eq!(m.to_display(), "12.34 USD");
        }
    
        #[test]
        fn test_money_negative() {
            let m = Money::new(-1234, "EUR");
            assert_eq!(m.to_display(), "-12.34 EUR");
        }
    
        #[test]
        fn test_money_from_dollars() {
            let m = Money::from_dollars(19.99, "USD");
            assert_eq!(m.cents, 1999);
        }
    
        #[test]
        fn test_money_json() {
            let m = Money::new(500, "GBP");
            assert_eq!(m.to_json(), r#"{"cents": 500, "currency": "GBP"}"#);
        }
    
        #[test]
        fn test_secret_redacted() {
            let secret = Secret::new("password123");
            let debug_output = format!("{:?}", secret);
            assert!(!debug_output.contains("password123"));
            assert!(debug_output.contains("REDACTED"));
        }
    
        #[test]
        fn test_secret_expose() {
            let secret = Secret::new(42);
            assert_eq!(*secret.expose(), 42);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_date_iso() {
            let date = Date::new(2024, 3, 15);
            assert_eq!(date.to_iso_string(), "2024-03-15");
        }
    
        #[test]
        fn test_date_compact() {
            let date = Date::new(2024, 3, 15);
            assert_eq!(date.to_compact(), 20240315);
        }
    
        #[test]
        fn test_date_roundtrip_iso() {
            let original = Date::new(2024, 12, 25);
            let s = original.to_iso_string();
            let parsed = Date::from_iso_string(&s).unwrap();
            assert_eq!(original, parsed);
        }
    
        #[test]
        fn test_date_roundtrip_compact() {
            let original = Date::new(2024, 1, 1);
            let n = original.to_compact();
            let parsed = Date::from_compact(n);
            assert_eq!(original, parsed);
        }
    
        #[test]
        fn test_money_display() {
            let m = Money::new(1234, "USD");
            assert_eq!(m.to_display(), "12.34 USD");
        }
    
        #[test]
        fn test_money_negative() {
            let m = Money::new(-1234, "EUR");
            assert_eq!(m.to_display(), "-12.34 EUR");
        }
    
        #[test]
        fn test_money_from_dollars() {
            let m = Money::from_dollars(19.99, "USD");
            assert_eq!(m.cents, 1999);
        }
    
        #[test]
        fn test_money_json() {
            let m = Money::new(500, "GBP");
            assert_eq!(m.to_json(), r#"{"cents": 500, "currency": "GBP"}"#);
        }
    
        #[test]
        fn test_secret_redacted() {
            let secret = Secret::new("password123");
            let debug_output = format!("{:?}", secret);
            assert!(!debug_output.contains("password123"));
            assert!(debug_output.contains("REDACTED"));
        }
    
        #[test]
        fn test_secret_expose() {
            let secret = Secret::new(42);
            assert_eq!(*secret.expose(), 42);
        }
    }

    Deep Comparison

    OCaml vs Rust: Custom Serialize Logic

    Custom Date Serialization

    Rust

    impl Date {
        pub fn to_iso_string(&self) -> String {
            format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
        }
        
        pub fn to_compact(&self) -> u32 {
            self.year as u32 * 10000 + self.month as u32 * 100 + self.day as u32
        }
    }
    

    OCaml

    let date_to_iso { year; month; day } =
      Printf.sprintf "%04d-%02d-%02d" year month day
    
    let date_to_compact { year; month; day } =
      year * 10000 + month * 100 + day
    

    Secret Values (Redaction)

    Rust

    impl<T> std::fmt::Debug for Secret<T> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "Secret([REDACTED])")
        }
    }
    

    OCaml

    type 'a secret = Secret of 'a
    
    let secret_to_string _ = "[REDACTED]"
    

    Key Differences

    AspectOCamlRust
    Custom Displayto_string functionDisplay trait
    Custom Debug%a formatDebug trait
    Newtype wrapperSingle variantStruct wrapper
    Format controlPrintf directivesformat! macros

    Exercises

  • Implement a Money { amount_cents: i64, currency: &str } type with custom serialization as "100.00 USD" and deserialization that parses that string format.
  • Write a custom serializer for IpAddr that serializes IPv4 as a 4-byte array and IPv6 as a 16-byte array in binary formats.
  • Implement serialize_with_version that adds a "_version": 2 field to any struct's JSON output, enabling format evolution detection during deserialization.
  • Open Source Repos