ExamplesBy LevelBy TopicLearning Paths
760 Fundamental

760-serde-derive-concept — Serde Derive Concept

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "760-serde-derive-concept — Serde Derive Concept" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. `#[derive(Serialize, Deserialize)]` is the most-used derive macro in the Rust ecosystem — the `serde` crate has been downloaded billions of times. Key difference from OCaml: 1. **Unified abstraction**: Rust's `serde::Serializer` trait enables one `#[derive(Serialize)]` to work with all formats; OCaml requires a separate `[@@deriving ...]` per format.

Tutorial

The Problem

#[derive(Serialize, Deserialize)] is the most-used derive macro in the Rust ecosystem — the serde crate has been downloaded billions of times. Understanding what the macro generates demystifies serde's design: it calls into a Serializer or Deserializer trait whose concrete implementation handles the format (JSON, TOML, MessagePack). This separation means your types are format-agnostic; the format is injected at the call site.

🎯 Learning Outcomes

  • • Understand the Visitor pattern that serde uses under the hood
  • • See what code #[derive(Serialize)] generates for a simple Point { x: i32, y: i32 } struct
  • • Implement a minimal Serializer trait with struct and primitive methods
  • • See how a JsonSerializer implements the trait to produce JSON output
  • • Understand why serde's trait-based design enables zero-cost abstraction across formats
  • Code Example

    // You write:
    #[derive(Serialize)]
    struct Point { x: i32, y: i32 }
    
    // The macro generates:
    impl Serialize for Point {
        fn serialize<S: Serializer>(&self, serializer: &mut S) {
            serializer.serialize_struct_start("Point", 2);
            serializer.serialize_field("x");
            self.x.serialize(serializer);
            serializer.serialize_field("y");
            self.y.serialize(serializer);
            serializer.serialize_struct_end();
        }
    }

    Key Differences

  • Unified abstraction: Rust's serde::Serializer trait enables one #[derive(Serialize)] to work with all formats; OCaml requires a separate [@@deriving ...] per format.
  • Visitor pattern: serde uses the Visitor pattern to drive deserialization; OCaml's derivers generate direct recursive descent parsers.
  • Zero cost: Both generate code equivalent to hand-written serialization; no runtime dispatch is needed.
  • Human-readable formats: Rust's serde handles both human-readable (JSON) and binary formats uniformly; OCaml uses sexp for human-readable and Bin_prot for binary.
  • OCaml Approach

    OCaml's ppx_sexp_conv generates sexp_of_t and t_of_sexp functions from a [@@deriving sexp] attribute — analogous to #[derive(Serialize, Deserialize)]. Bin_prot generates bin_write_t / bin_read_t pairs. ppx_yojson_conv generates JSON serializers. Unlike Rust's unified Serializer trait, OCaml uses separate functions per format, generated by separate ppx packages.

    Full Source

    #![allow(clippy::all)]
    //! # Serde Derive Concept
    //!
    //! Understanding how derive macros generate serialization code.
    
    /// Simulated derive output for a struct
    ///
    /// Given:
    /// ```ignore
    /// #[derive(Serialize)]
    /// struct Point { x: i32, y: i32 }
    /// ```
    ///
    /// The derive macro generates something like this implementation.
    
    // Manual implementation showing what #[derive(Serialize)] would generate
    
    /// A simple output trait (simulating serde's Serializer)
    pub trait Serializer {
        fn serialize_i32(&mut self, v: i32);
        fn serialize_str(&mut self, v: &str);
        fn serialize_struct_start(&mut self, name: &str, len: usize);
        fn serialize_field(&mut self, name: &str);
        fn serialize_struct_end(&mut self);
    }
    
    /// Our serialize trait
    pub trait Serialize {
        fn serialize<S: Serializer>(&self, serializer: &mut S);
    }
    
    // Primitive implementations
    impl Serialize for i32 {
        fn serialize<S: Serializer>(&self, serializer: &mut S) {
            serializer.serialize_i32(*self);
        }
    }
    
    impl Serialize for String {
        fn serialize<S: Serializer>(&self, serializer: &mut S) {
            serializer.serialize_str(self);
        }
    }
    
    impl Serialize for &str {
        fn serialize<S: Serializer>(&self, serializer: &mut S) {
            serializer.serialize_str(self);
        }
    }
    
    /// Example struct
    #[derive(Debug, PartialEq)]
    pub struct Point {
        pub x: i32,
        pub y: i32,
    }
    
    /// What #[derive(Serialize)] would generate for Point
    impl Serialize for Point {
        fn serialize<S: Serializer>(&self, serializer: &mut S) {
            serializer.serialize_struct_start("Point", 2);
            serializer.serialize_field("x");
            self.x.serialize(serializer);
            serializer.serialize_field("y");
            self.y.serialize(serializer);
            serializer.serialize_struct_end();
        }
    }
    
    /// Another example struct
    #[derive(Debug, PartialEq)]
    pub struct Person {
        pub name: String,
        pub age: i32,
    }
    
    impl Serialize for Person {
        fn serialize<S: Serializer>(&self, serializer: &mut S) {
            serializer.serialize_struct_start("Person", 2);
            serializer.serialize_field("name");
            self.name.serialize(serializer);
            serializer.serialize_field("age");
            self.age.serialize(serializer);
            serializer.serialize_struct_end();
        }
    }
    
    // A JSON-like serializer for testing
    pub struct JsonSerializer {
        output: String,
        first_field: bool,
    }
    
    impl JsonSerializer {
        pub fn new() -> Self {
            JsonSerializer {
                output: String::new(),
                first_field: true,
            }
        }
    
        pub fn into_string(self) -> String {
            self.output
        }
    }
    
    impl Default for JsonSerializer {
        fn default() -> Self {
            Self::new()
        }
    }
    
    impl Serializer for JsonSerializer {
        fn serialize_i32(&mut self, v: i32) {
            self.output.push_str(&v.to_string());
        }
    
        fn serialize_str(&mut self, v: &str) {
            self.output.push('"');
            self.output.push_str(v);
            self.output.push('"');
        }
    
        fn serialize_struct_start(&mut self, _name: &str, _len: usize) {
            self.output.push('{');
            self.first_field = true;
        }
    
        fn serialize_field(&mut self, name: &str) {
            if !self.first_field {
                self.output.push_str(", ");
            }
            self.first_field = false;
            self.output.push('"');
            self.output.push_str(name);
            self.output.push_str("\": ");
        }
    
        fn serialize_struct_end(&mut self) {
            self.output.push('}');
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_serialize_point() {
            let point = Point { x: 10, y: 20 };
            let mut ser = JsonSerializer::new();
            point.serialize(&mut ser);
            assert_eq!(ser.into_string(), r#"{"x": 10, "y": 20}"#);
        }
    
        #[test]
        fn test_serialize_person() {
            let person = Person {
                name: "Alice".to_string(),
                age: 30,
            };
            let mut ser = JsonSerializer::new();
            person.serialize(&mut ser);
            assert_eq!(ser.into_string(), r#"{"name": "Alice", "age": 30}"#);
        }
    
        #[test]
        fn test_serialize_i32() {
            let mut ser = JsonSerializer::new();
            42i32.serialize(&mut ser);
            assert_eq!(ser.into_string(), "42");
        }
    
        #[test]
        fn test_serialize_string() {
            let mut ser = JsonSerializer::new();
            "hello".serialize(&mut ser);
            assert_eq!(ser.into_string(), "\"hello\"");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_serialize_point() {
            let point = Point { x: 10, y: 20 };
            let mut ser = JsonSerializer::new();
            point.serialize(&mut ser);
            assert_eq!(ser.into_string(), r#"{"x": 10, "y": 20}"#);
        }
    
        #[test]
        fn test_serialize_person() {
            let person = Person {
                name: "Alice".to_string(),
                age: 30,
            };
            let mut ser = JsonSerializer::new();
            person.serialize(&mut ser);
            assert_eq!(ser.into_string(), r#"{"name": "Alice", "age": 30}"#);
        }
    
        #[test]
        fn test_serialize_i32() {
            let mut ser = JsonSerializer::new();
            42i32.serialize(&mut ser);
            assert_eq!(ser.into_string(), "42");
        }
    
        #[test]
        fn test_serialize_string() {
            let mut ser = JsonSerializer::new();
            "hello".serialize(&mut ser);
            assert_eq!(ser.into_string(), "\"hello\"");
        }
    }

    Deep Comparison

    OCaml vs Rust: Serde Derive Concept

    What Derive Generates

    Rust

    // You write:
    #[derive(Serialize)]
    struct Point { x: i32, y: i32 }
    
    // The macro generates:
    impl Serialize for Point {
        fn serialize<S: Serializer>(&self, serializer: &mut S) {
            serializer.serialize_struct_start("Point", 2);
            serializer.serialize_field("x");
            self.x.serialize(serializer);
            serializer.serialize_field("y");
            self.y.serialize(serializer);
            serializer.serialize_struct_end();
        }
    }
    

    OCaml (ppx_deriving)

    (* You write: *)
    type point = { x: int; y: int } [@@deriving yojson]
    
    (* The ppx generates: *)
    let point_to_yojson p =
      `Assoc [
        ("x", `Int p.x);
        ("y", `Int p.y)
      ]
    

    Serializer Trait Pattern

    Rust

    pub trait Serializer {
        fn serialize_i32(&mut self, v: i32);
        fn serialize_str(&mut self, v: &str);
        fn serialize_struct_start(&mut self, name: &str, len: usize);
        fn serialize_field(&mut self, name: &str);
        fn serialize_struct_end(&mut self);
    }
    

    Key Differences

    AspectOCamlRust
    Macro systemPPXProc macros
    Type-drivenppx_derivingserde derive
    Output formatSpecific (yojson)Generic (Serializer trait)
    Format agnosticMultiple ppx neededSingle Serialize impl

    Exercises

  • Implement a TomlSerializer that produces TOML output for simple structs (key = value pairs), and verify it produces correct output for Point { x: 1, y: 2 }.
  • Add serialize_vec to the Serializer trait and implement it for JsonSerializer as a JSON array, then implement Serialize for Vec<T: Serialize>.
  • Write the "generated" Deserialize implementation for Point — what the visitor-based deserialization looks like for a two-field struct.
  • Open Source Repos