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.