---
title: Serialization
source: 'https://academia.sh/en/courses/rust-abstraction/serialization'
course: 'Traits, Generics and Error Handling'
language: en
updated: '2026-10-21T05:03:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Shows, by running it, how many times per field a transformation body applied to a struct's fields runs, and how that count grows in a nested struct"
    - "Verifies by measurement that in text-to-struct parsing, one field's failure never runs the following fields' bodies"
    - "Compares every body measured across the course's fifteen lessons in a single closing table, grouped by class"
---

# Serialization

How many times a hand-written transformation body applied to a struct's fields runs per field, how that count grows multiplicatively in a nested struct, and how, in the reverse direction — from text back to a struct — one field's failure never runs the following fields' bodies at all; every body measured across the course's fifteen lessons is gathered here into a single table.

The Iterator Adapters lesson measured how many times the consumer at the end of a chain
calls a transformation body in the source — the chain's length stayed fixed while the
number of bodies that ran varied with the consumer. This closing lesson carries the same
question into a concrete data format: how many times per field does a transformation body
applied to a struct's fields run while the struct is serialized. Here, the chain's elements
are no longer a source's entries but a single struct's fields; the consumer, too, is
singular — serialization itself. The transformation is hand-written in this lesson, with no
third-party crate in use; a serialization framework would produce this body on its own, but
here the body stays visible in the source and countable. The question asked throughout the
course does not change here either: how many times did this body you wrote run? This is the
course's fifteenth and closing lesson; its close gathers the bodies measured across all
fourteen lessons into a single table.

## A Transformation Body That Runs Per Field

Had there been a serialization framework, it would have produced this transformation on its
own, invisibly growing the body as the field count rose. This lesson writes the
transformation by hand: every field passes through the same helper function to become a
string, and this pass drops one entry into the count ledger every time. This slice of the
measurement core carries only the counting ledger; no trait, bound, or error type is used
in this lesson.

```rust
// times.rs — excerpt: the counting ledger.

use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

pub type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

pub fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

pub fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}
```

A three-field `Point` struct carries a `serialize` method that turns its fields into a
string in order; every field passes through the same `write_field` function, and each pass
is counted. `Line` is a struct carrying two `Point` values; its own `serialize` does not
touch a field of its own, it calls the `serialize` of the two `Point` values it carries.
`Triangle` is a struct carrying three `Line` values; the same pattern goes one level deeper.

```rust
// field_body.rs — measures how many times a body runs per field, shows multiplicative growth in nesting.
mod times;
use times::{times, record, Ledger};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

fn write_field(ledger: &Ledger, name: &str, value: i32) -> String {
    record(ledger, "field-body");
    format!("{}={}", name, value)
}

struct Point {
    x: i32,
    y: i32,
    z: i32,
}

impl Point {
    fn serialize(&self, ledger: &Ledger) -> String {
        let x = write_field(ledger, "x", self.x);
        let y = write_field(ledger, "y", self.y);
        let z = write_field(ledger, "z", self.z);
        format!("{};{};{}", x, y, z)
    }
}

struct Line {
    start: Point,
    end: Point,
}

impl Line {
    fn serialize(&self, ledger: &Ledger) -> String {
        format!("{}|{}", self.start.serialize(ledger), self.end.serialize(ledger))
    }
}

struct Triangle {
    edge1: Line,
    edge2: Line,
    edge3: Line,
}

impl Triangle {
    fn serialize(&self, ledger: &Ledger) -> String {
        format!(
            "{}#{}#{}",
            self.edge1.serialize(ledger),
            self.edge2.serialize(ledger),
            self.edge3.serialize(ledger)
        )
    }
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));

    let before = times(&ledger, "field-body");
    let point = Point { x: 1, y: 2, z: 3 };
    let _ = point.serialize(&ledger);
    let after = times(&ledger, "field-body");
    println!("Point (3 fields): {} body runs", after - before);

    let before = after;
    let line = Line {
        start: Point { x: 0, y: 0, z: 0 },
        end: Point { x: 5, y: 5, z: 5 },
    };
    let _ = line.serialize(&ledger);
    let after = times(&ledger, "field-body");
    println!("Line (2 points, 6 fields): {} body runs", after - before);

    let before = after;
    let triangle = Triangle {
        edge1: Line {
            start: Point { x: 0, y: 0, z: 0 },
            end: Point { x: 1, y: 1, z: 1 },
        },
        edge2: Line {
            start: Point { x: 1, y: 1, z: 1 },
            end: Point { x: 2, y: 2, z: 2 },
        },
        edge3: Line {
            start: Point { x: 2, y: 2, z: 2 },
            end: Point { x: 0, y: 0, z: 0 },
        },
    };
    let _ = triangle.serialize(&ledger);
    let after = times(&ledger, "field-body");
    println!("Triangle (3 lines, 18 fields): {} body runs", after - before);

    println!("total field-body runs: {}", times(&ledger, "field-body"));
}
```

```
Point (3 fields): 3 body runs
Line (2 points, 6 fields): 6 body runs
Triangle (3 lines, 18 fields): 18 body runs
total field-body runs: 27
```

**CI11.** `Point` carries three fields, and its `serialize` drops exactly three
`field-body` entries — one to one. `Line` does not touch a field of its own at all, it only
calls the `serialize` of its two `Point` values; but since each call carries its own three
entries, the total comes to six. `Triangle` carries the same pattern one level deeper: each
of its three `Line` values leaves six entries, for a total of eighteen. The relationship
between field count and body run count is one to one here — but this relationship only
holds for the leaf-level actual fields (`x`, `y`, `z`); `Line` and `Triangle`'s own bodies
never leave a `field-body` entry themselves, they only delegate to the sub-structures they
carry. Nesting grows the count multiplicatively rather than additively: `Triangle`'s three
edges each multiply their six entries.

## Parsing in Reverse: One Field's Failure Stops the Rest

The same question can also be asked in reverse: how many bodies run per field to turn a
string back into a struct? Parsing, too, is written by hand — every field passes through a
shared `read_field` function; this function records first, then tries to parse the piece of
the string. `Point::deserialize` reads its three fields in order and places a question mark
after each read call — the early return measured in the Shorthand for Error Propagation
lesson holds here as well: if one field fails, the question mark exits the function without
ever running the remaining fields' bodies.

```rust
// parsing.rs — reverse direction: text to struct, body runs per field.
mod times;
use times::{times, record, Ledger};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

struct ParseError;

struct Point {
    x: i32,
    y: i32,
    z: i32,
}

fn read_field(ledger: &Ledger, name: &str, key: &str, part: &str) -> Result<i32, ParseError> {
    record(ledger, key);
    let pair: Vec<&str> = part.splitn(2, '=').collect();
    if pair.len() != 2 || pair[0] != name {
        return Err(ParseError);
    }
    pair[1].parse::<i32>().map_err(|_| ParseError)
}

impl Point {
    fn deserialize(ledger: &Ledger, prefix: &str, text: &str) -> Result<Point, ParseError> {
        let parts: Vec<&str> = text.split(';').collect();
        if parts.len() != 3 {
            return Err(ParseError);
        }
        let x = read_field(ledger, "x", &format!("{}-x", prefix), parts[0])?;
        let y = read_field(ledger, "y", &format!("{}-y", prefix), parts[1])?;
        let z = read_field(ledger, "z", &format!("{}-z", prefix), parts[2])?;
        Ok(Point { x, y, z })
    }
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));

    let valid = Point::deserialize(&ledger, "valid", "x=1;y=2;z=3");
    if let Ok(p) = &valid {
        println!("valid text parsed: x={} y={} z={}", p.x, p.y, p.z);
    }
    println!(
        "valid-x: {}  valid-y: {}  valid-z: {}",
        times(&ledger, "valid-x"),
        times(&ledger, "valid-y"),
        times(&ledger, "valid-z")
    );

    let invalid = Point::deserialize(&ledger, "invalid", "x=1;y=not;z=3");
    println!("did invalid text succeed: {}", invalid.is_ok());
    println!(
        "invalid-x: {}  invalid-y: {}  invalid-z: {}",
        times(&ledger, "invalid-x"),
        times(&ledger, "invalid-y"),
        times(&ledger, "invalid-z")
    );
}
```

```
valid text parsed: x=1 y=2 z=3
valid-x: 1  valid-y: 1  valid-z: 1
did invalid text succeed: false
invalid-x: 1  invalid-y: 1  invalid-z: 0
```

**CI12.** For the valid string (`x=1;y=2;z=3`), all three fields are read and three separate
counters each go up by one — the forward direction's one-to-one relationship holds here too.
For the invalid string (`x=1;y=not;z=3`), the `x` field is read and succeeds, and the `y`
field is also read but fails to parse as a number — both of their counters go up by one,
because both were at least attempted. The `z` field's counter stays at zero: the question
mark exits the function the moment it catches `y`'s failure, and the body written for `z` is
never touched. This shows that the forward direction's **every time** class does not stay
unconditional in reverse: a field's body becomes dependent on the fields before it
succeeding.

## A Bounding Measurement

The one-to-one relationship between field count and body run count holds in this lesson
only because every struct used here carries unconditional fields — every field always
exists, always gets transformed. Had the type of absence measured in the Expressing
Absence with a Type lesson been a field itself, that field's transformation body would no
longer run unconditionally: it would run for some values and not for others, and counting
the fields alone would no longer give the body run count. This lesson does not run that
case, it only states under which condition the relationship holds and under which it
breaks.

A second boundary concerns what the count does not measure. The `field-body` counter only
counts how many times the body was called — it does not measure how long the string it
produced is, or how expensive one field's transformation was. A single integer field and a
field carrying a large collection are the same to this counter — both are counted once. This
distinction, carried since the start of the course, holds here too: the number only shows
how many times something ran, not how long it took or what it produced.

## Closing Table

| Lesson | Body measured | Class | Bounding measurement |
|---|---|---|---|
| Trait Definition and Implementation | Default trait body (`name`) | every time · never | Zero runs does not mean the body is absent from the source |
| Trait Bounds | Run count of the bounded and unbounded body | every time | The cost is at compile time: a separate body per type, unmeasurable by running |
| Associated Types | Associated-type / generic-parameter calls | every time | The difference is not in the run but in what the caller must write |
| Object Safety and Dynamic Dispatch | `static-dispatch` / `dynamic-dispatch` | every time | Same count is not the same cost; how the call is resolved is not measurable |
| Derived Traits | Hand-written / derived equality body | every time (hand-written) · uncountable (derived) | The derived body is not written in the source, its run count cannot be counted |
| Operator Overloading | `operator-body` (`add`) | every time | The call site is invisible in the source; the count is only known from the ledger |
| Expressing Absence with a Type | Eager / lazy default body | every time · only when needed · never | If the body is cheap the two forms give the same result; only the count differs |
| Failure as a Return Value | Success / failure branch | only when needed · never | Covered code and executed code are separate facts |
| The Shorthand for Error Propagation | Body after the question mark | only when needed · never | Early return silently skips the rest; the side effect also never happens |
| Custom Error Types | Error conversion body (`From` / `map_err`) | every time | The implicit conversion cannot access the caller's context; the loss is irreversible |
| Panic and Unrecoverable Errors | Remaining body (recoverable/unrecoverable) | every time · never | Panic is a design decision, not an error type; no panic text was printed |
| Closures | Uncalled closure body | never | A closure's own definition is separate work that never enters the ledger |
| The Iterator Trait | Unconsumed chain's body | never | An unconsumed chain is not free; its cost cannot be measured by the run counter |
| Iterator Adapters | Consumed / short-circuiting chain body | every time · only when needed | Order is not read from the source, only observed with a trace |
| Serialization | Per-field transformation body | every time · never | The relationship holds only when every field is unconditional; a conditional field changes the class |

## Summary

- A transformation body applied to a struct's fields runs once per field; in a struct with
  n fields, this body runs n times — three for `Point`, six for `Line` (which carries two
  `Point`s), eighteen for `Triangle` (which carries three `Line`s).
- In a nested struct, field count grows multiplicatively (3, 6, 18), and the body's run
  count tracks this growth exactly.
- This body falls into the **every time** class: every field is inevitably transformed
  every time serialization is called.
- In the reverse direction — text to struct — when one field's parsing fails, the question
  mark silently skips the remaining fields' body; these fields never run in that run.
- If a field is not unconditional (if it carries a type of absence), this relationship
  breaks; counting fields no longer gives the body run count.
- Across fifteen lessons, every body measured settled into three classes (every time, only
  when needed, never); the closing table gathers them in one place.

## Course Wrap-Up

Across fifteen lessons, this course applied a single question to one mechanism after
another: how many times did this body you wrote run? The Traits and Generics
topic asked this question of a behavior separating from its type, of whether a
bound adds anything to the run, of a contract's two different forms, of whether a dispatch
form is resolved at compile time or at run time, of a body being able to run without ever
being written in the source at all, and of a call hidden behind an operator. The Error
Handling topic carried the same question to a fallback value, a success/failure branch, the
branching behind a shorthand, an error conversion, and an unrecoverable state. Closures and
Iterators, and this closing lesson, applied the same question to a definition itself, a
traversal contract, a consumer at the end of a chain, and now to a struct's fields. Of the
sixteen bodies in the measurement core, eight ran every time, three ran only when needed,
and five never ran — this number did not change across the course, only which mechanism
fell into which class changed.

The Concurrency and Asynchronous Rust course does not drop this question, it changes its
subject: instead of how many times a body ran, it will ask what a task needs in order to
make progress — whether it advances on its own, is advanced by a poller, waits for something
else to finish, or does not advance at all. Next to the Object-Oriented Java course's
question of which type made the decision, and the Interfaces, Errors and Generics course's
question of where the bond forms, this course added the question of how many times
something ran; the next course will add to this trio the question of what is needed in
order to make progress.
