5. User-Defined Types

5.1. Serialization by ordinary functions

An application extends xmlio by defining read and write beside its own type. No base class, registration macro, or modification of xmlio is required.

 1/**
 2 * @file custom_types.cpp
 3 * @brief Serialize a vector of small user-defined records with xmlio.
 4 */
 5
 6#include <xmlio/xmlio.hpp>
 7
 8#include <iostream>
 9#include <string>
10#include <vector>
11
12namespace example {
13
14struct Task {
15  std::string name;
16  int priority{};
17};
18
19void read(xmlio::XMLReader& xml, std::string_view path, Task& task) {
20  xmlio::XMLReader node(xml, path);
21  xmlio::read(node, "name", task.name);
22  xmlio::read(node, "priority", task.priority);
23}
24
25void write(xmlio::XMLWriter& xml, std::string_view path, const Task& task) {
26  xmlio::push(xml, path);
27  xmlio::write(xml, "name", task.name);
28  xmlio::write(xml, "priority", task.priority);
29  xmlio::pop(xml);
30}
31
32} // namespace example
33
34int main() {
35  const std::vector<example::Task> tasks{{"prepare", 2}, {"finish", 1}};
36
37  xmlio::XMLBufferWriter output("project");
38  xmlio::write(output, "tasks", tasks);
39
40  auto input = xmlio::XMLReader::from_string(output.str());
41  const auto restored =
42      xmlio::read<std::vector<example::Task>>(input, "/project/tasks");
43
44  for (const auto& task : restored) {
45    std::cout << task.name << " has priority " << task.priority << '\n';
46  }
47}

The functions live in namespace example beside Task. Argument-dependent lookup finds them when the generic vector serializer encounters a Task. The resulting XML is:

<project>
  <tasks>
    <elem>
      <name>prepare</name>
      <priority>2</priority>
    </elem>
    <elem>
      <name>finish</name>
      <priority>1</priority>
    </elem>
  </tasks>
</project>

5.2. Required and optional fields

Use ordinary read for required fields. Use read_optional or std::optional only when absence is valid according to the type’s schema:

struct Task {
  std::string name;
  int priority{};
  std::optional<std::string> note;
};

void read(xmlio::XMLReader& xml, std::string_view path, Task& task) {
  xmlio::XMLReader node(xml, path);
  xmlio::read(node, "name", task.name);
  xmlio::read(node, "priority", task.priority);
  xmlio::read(node, "note", task.note);
}

A malformed present value remains an error. Optional reading should not be used to hide conversion failures.

5.3. Enums and readable XML

Enums are best represented by stable names rather than underlying integers:

enum class Mode { fast, careful };

void write(xmlio::XMLWriter& xml, std::string_view path, Mode mode) {
  xmlio::write(xml, path, mode == Mode::fast ? "fast" : "careful");
}

void read(xmlio::XMLReader& xml, std::string_view path, Mode& mode) {
  const auto text = xmlio::read<std::string>(xml, path);
  if (text == "fast") {
    mode = Mode::fast;
  } else if (text == "careful") {
    mode = Mode::careful;
  } else {
    throw xmlio::ConversionError(path, text, "Mode");
  }
}

This keeps hand-written files understandable and prevents enum reordering from changing the file format.

5.4. Validation belongs to the type

xmlio verifies XML structure and conversion. Domain validation belongs in the owning type or in a dedicated validation function after deserialization:

if (task.priority < 0) {
  throw xmlio::QueryError(path, "priority must be non-negative");
}

Keeping this boundary explicit lets the same serialization machinery serve many packages without teaching xmlio their domain rules.

5.5. Nested and associative records

Once a type has read and write, it can appear recursively inside vector, list, array, pair, and map values. Map keys must also satisfy the ordering requirements of std::map. Deeply nested formats remain readable when each owning type gives its children meaningful tag names.