Skip to content

Advanced Project 9 — Serialization Framework

Difficulty: Advanced · Estimated effort: 2–3 weeks · Prerequisites: Module 12 — Templates, Module 13 — Exception Handling, Module 15 — SOLID and Design Patterns · Demanded pattern decisions: visitor, factory

This is one of the designated integration projects, and one of the projects that requires a design-pattern decision: at least two patterns must be named and justified by a requirement, and one alternative must be considered and declined in writing.

No complete solution is provided. How a type declares its own serialisation is the central design question here, and it is deliberately unanswered.


1. Scenario

A department stores several kinds of record in text files and keeps writing the same save/load code by hand for each one, with three recurring bugs: the file format drifts between records, a field added to a struct is forgotten in both directions, and a corrupt file crashes the loader instead of reporting a line number.

You are asked for one small framework so a type says what it has, once, and gets saving, loading and validation from that statement.


2. Problem statement

Build a serialization framework in which a type describes its members and the framework derives the text format, a version marker, and robust loading. Round-trip fidelity must be a property you test, corrupt input must be reported with a line number, and adding a member must be a change in one place.


3. User requirements

  • [ ] Save any registered type to a text file and load it back unchanged.
  • [ ] Save and load collections of that type, including an empty one.
  • [ ] Be told the line number and the field name for a corrupt or missing field.
  • [ ] Add or reorder a field and get a clear, early failure when loading an older file.
  • [ ] Use the framework with a nested type (a record containing another record).
  • [ ] See the produced text file, in a format a human can read and hand-edit.

4. Functional requirements

  • [ ] A description of a type's members, with names, kinds and ordering.
  • [ ] save and load functions driven by that description, in at least two of: a field-per-line format, a key=value format, a brace-delimited format.
  • [ ] Support for the fundamental types, std::string, and at least one nested described type.
  • [ ] Collections: a described type that contains a std::vector of primitives or of nested types.
  • [ ] A version marker and a documented compatibility rule.
  • [ ] Line-numbered errors distinguishing missing field, unexpected field and bad value.
  • [ ] A creation point turning a format name into the right writer and reader — which is where the factory decision lives.

5. Non-functional requirements

  • [ ] Round trip is exact: the reloaded object equals the original, for every registered type and including empty collections and strings containing the separator.
  • [ ] A failed load throws with a line number and leaves the caller's object untouched.
  • [ ] Adding one member to a described type requires editing exactly one function in that type's file — and you must demonstrate that with a real addition and a file count.
  • [ ] No reflection tricks outside the standard library; the framework is portable C++17.
  • [ ] Builds warning-free with g++ -std=c++17 -Wall -Wextra -Werror.

6. Suggested features

  • A second format behind the same description, chosen at run time.
  • A describe() output that prints the schema, useful as documentation and as a test fixture.
  • A migration hook: when the version is older, a named function upgrades the record.
  • A length-prefixed container encoding, with a note on why variable-length records need one.

7. OOP concepts involved

Concept Required Where it appears
Multiple classes yes Member descriptors, described types, writers, readers, factory, error types, examples
Relationships yes A type ──> its description; writer ──> an output stream; framework ◆ the format's components
Encapsulation yes Described types keep their invariants: loading constructs a valid object or fails
Inheritance or composition yes Described types have a description rather than deriving a base per format — and you must justify that against the alternative
Polymorphism yes A visitor over member descriptors so one algorithm handles every member kind
The STL yes std::vector for members, std::variant/std::function for member access, <sstream> for parsing
Exception handling yes Three distinct load errors with field names and line numbers

8. Suggested design considerations

The pattern decision you must make and justify

Visitor, for walking a record's members. Saving and loading are two algorithms over the same structure — a record's list of members — and each member is of a different kind. A visitor over the member descriptors gives one algorithm per operation instead of one branch per kind per operation. The alternative is polymorphic save/load methods on each member descriptor; argue which you chose and what the trade-off was.

Factory, for format selection. The format is chosen by name, and a new format must not require editing the framework. The alternative is a small registry of functions. Pick one and decline the other in writing — that is the declined-pattern item.

Ideas worth weighing and recording: template method for the write/read skeleton (likely earned); memento for the "load fails, restore" path (avoid by construction instead, and say why); interpreter for a self-describing format (not required).

Other design points

The "one place" claim is the acceptance test. Add a member and count the files. If it takes two files, the description is not the single source of truth and the framework has failed its own purpose — which is a legitimate finding for your report, if you say so.

Design the failure path first. A corrupt file is not an edge case here; it is most of the requirement. Work out what each error message contains before writing the loader.

Round trip includes the awkward cases. An empty vector, a string containing the separator, and a zero value are the three cases that catch most encoding bugs. Put them in the test suite first, not last.


9. Minimum viable version

A member description for one record type, a field-per-line format, save and load for that one type, and round-trip equality for a hand-built instance.


10. Intermediate extension

Add nested and collection members, the version marker, and the three line-numbered error types. Then run the round trip over a set of awkward values: empty strings, empty collections, values at the maximum of their type, and a string containing the separator.


11. Advanced extension

Add the second format, the migration hook and the describe output. Then add a member to a described type and record exactly how many files had to change, against your prediction. Then write the design note: the patterns, the declined alternative, and the decision you would reverse.


12. Testing checklist

  • [ ] Round trip of a simple record with defaults preserves every field
  • [ ] Round trip of an empty collection, and of a collection with one element, both preserve size
  • [ ] A string containing the separator round-trips through the documented escape
  • [ ] A nested record round-trips, and its inner fields are validated with line numbers
  • [ ] A missing field is reported as missing, naming the field and the line
  • [ ] An unexpected extra field is reported, not ignored
  • [ ] A field with a bad value names the field, the bad text and the conversion that failed
  • [ ] A file with the wrong version is reported per the documented compatibility rule
  • [ ] A failed load leaves the caller's object byte-for-byte unchanged
  • [ ] Adding one member required editing exactly one function — the file count is recorded
  • [ ] Both formats, loaded from the same record, produce equal objects (where lossless)
  • [ ] Save → load → save produces identical files, for both formats

13. Documentation checklist

  • [ ] How to register a type, as a short worked example with its output
  • [ ] The formats, with a full file example and the escaping rule
  • [ ] The compatibility and version rules
  • [ ] The error catalogue: message shape for each of the three load errors
  • [ ] The patterns used and declined, with the requirement each served

14. Self-evaluation rubric

Criterion Excellent looks like Weight
Single source of truth Adding a member is one edit; measured and reported 25%
Round-trip fidelity Awkward values verified, including empty containers and separators 25%
Pattern decisions Visitor and factory justified; one alternative declined with a requirement 20%
Failure design Three typed load errors with field names and line numbers; no partial state 20%
Formats A second format behind the same description, with the factory keeping the framework clean 5%
Documentation Registration example, format specification and error catalogue 5%

Related: Advanced projects · Project roadmap · All projects · Module 12 — Templates