761-custom-serialize-logic — Custom Serialize Logic
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
Date type as ISO 8601 string vs. compact integerOption fields as null/value with custom null representationpassword_hash) during serializationDate valuesCode 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(skip)], #[serde(rename = "...")], #[serde(with = "...")] provide common custom behaviors without full manual implementation; OCaml's equivalents are [@yojson.key], [@sexp.opaque].Serialize impl works across all serde formats; OCaml's custom functions are typically format-specific.#[serde(with = "timestamp_seconds")] delegates to a module with serialize/deserialize functions; OCaml has no direct equivalent.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);
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| Custom Display | to_string function | Display trait |
| Custom Debug | %a format | Debug trait |
| Newtype wrapper | Single variant | Struct wrapper |
| Format control | Printf directives | format! macros |
Exercises
Money { amount_cents: i64, currency: &str } type with custom serialization as "100.00 USD" and deserialization that parses that string format.IpAddr that serializes IPv4 as a 4-byte array and IPv6 as a 16-byte array in binary formats.serialize_with_version that adds a "_version": 2 field to any struct's JSON output, enabling format evolution detection during deserialization.