FileParse is designed to be extensible. You can create serializers for any custom type by defining the appropriate operators.
#include <fileParse/Common.hxx>
#include <fileParse/Vector.hxx>
#include <fileParse/Optional.hxx>
#include <fileParse/FileDataHandler.hxx>struct Point {
double x;
double y;
double z;
};template<typename NodeAdapter>
NodeAdapter& operator<<(NodeAdapter& node, const Point& p) {
node << FileParse::Child{"X", p.x};
node << FileParse::Child{"Y", p.y};
node << FileParse::Child{"Z", p.z};
return node;
}
template<typename NodeAdapter>
const NodeAdapter& operator>>(const NodeAdapter& node, Point& p) {
node >> FileParse::Child{"X", p.x};
node >> FileParse::Child{"Y", p.y};
node >> FileParse::Child{"Z", p.z};
return node;
}<Point>
<X>1.5</X>
<Y>2.5</Y>
<Z>3.5</Z>
</Point>Custom types can contain other custom types:
struct Line {
Point start;
Point end;
std::string label;
};
template<typename NodeAdapter>
NodeAdapter& operator<<(NodeAdapter& node, const Line& l) {
node << FileParse::Child{"Start", l.start};
node << FileParse::Child{"End", l.end};
node << FileParse::Child{"Label", l.label};
return node;
}
template<typename NodeAdapter>
const NodeAdapter& operator>>(const NodeAdapter& node, Line& l) {
node >> FileParse::Child{"Start", l.start};
node >> FileParse::Child{"End", l.end};
node >> FileParse::Child{"Label", l.label};
return node;
}<Line>
<Start>
<X>0</X>
<Y>0</Y>
<Z>0</Z>
</Start>
<End>
<X>10</X>
<Y>10</Y>
<Z>10</Z>
</End>
<Label>Diagonal</Label>
</Line>Once you define serializers for a type, it automatically works with containers:
struct Shape {
std::string name;
std::vector<Point> vertices;
};
template<typename NodeAdapter>
NodeAdapter& operator<<(NodeAdapter& node, const Shape& s) {
node << FileParse::Child{"Name", s.name};
node << FileParse::Child{{"Vertices", "Point"}, s.vertices};
return node;
}
template<typename NodeAdapter>
const NodeAdapter& operator>>(const NodeAdapter& node, Shape& s) {
node >> FileParse::Child{"Name", s.name};
node >> FileParse::Child{{"Vertices", "Point"}, s.vertices};
return node;
}<Shape>
<Name>Triangle</Name>
<Vertices>
<Point>
<X>0</X>
<Y>0</Y>
<Z>0</Z>
</Point>
<Point>
<X>1</X>
<Y>0</Y>
<Z>0</Z>
</Point>
<Point>
<X>0.5</X>
<Y>1</Y>
<Z>0</Z>
</Point>
</Vertices>
</Shape>struct DatabaseConfig {
std::string host;
int port;
std::string username;
std::optional<std::string> password;
};
struct LoggingConfig {
std::string level;
std::string outputPath;
bool enabled;
};
struct AppConfig {
std::string appName;
std::string version;
DatabaseConfig database;
LoggingConfig logging;
std::vector<std::string> features;
};
// DatabaseConfig serializers
template<typename NodeAdapter>
NodeAdapter& operator<<(NodeAdapter& node, const DatabaseConfig& d) {
node << FileParse::Child{"Host", d.host};
node << FileParse::Child{"Port", d.port};
node << FileParse::Child{"Username", d.username};
node << FileParse::Child{"Password", d.password};
return node;
}
template<typename NodeAdapter>
const NodeAdapter& operator>>(const NodeAdapter& node, DatabaseConfig& d) {
node >> FileParse::Child{"Host", d.host};
node >> FileParse::Child{"Port", d.port};
node >> FileParse::Child{"Username", d.username};
node >> FileParse::Child{"Password", d.password};
return node;
}
// LoggingConfig serializers
template<typename NodeAdapter>
NodeAdapter& operator<<(NodeAdapter& node, const LoggingConfig& l) {
node << FileParse::Child{"Level", l.level};
node << FileParse::Child{"OutputPath", l.outputPath};
node << FileParse::Child{"Enabled", l.enabled};
return node;
}
template<typename NodeAdapter>
const NodeAdapter& operator>>(const NodeAdapter& node, LoggingConfig& l) {
node >> FileParse::Child{"Level", l.level};
node >> FileParse::Child{"OutputPath", l.outputPath};
node >> FileParse::Child{"Enabled", l.enabled};
return node;
}
// AppConfig serializers
template<typename NodeAdapter>
NodeAdapter& operator<<(NodeAdapter& node, const AppConfig& a) {
node << FileParse::Child{"AppName", a.appName};
node << FileParse::Child{"Version", a.version};
node << FileParse::Child{"Database", a.database};
node << FileParse::Child{"Logging", a.logging};
node << FileParse::Child{{"Features", "Feature"}, a.features};
return node;
}
template<typename NodeAdapter>
const NodeAdapter& operator>>(const NodeAdapter& node, AppConfig& a) {
node >> FileParse::Child{"AppName", a.appName};
node >> FileParse::Child{"Version", a.version};
node >> FileParse::Child{"Database", a.database};
node >> FileParse::Child{"Logging", a.logging};
node >> FileParse::Child{{"Features", "Feature"}, a.features};
return node;
}AppConfig config{
"MyApp",
"1.0.0",
{"localhost", 5432, "admin", "secret123"},
{"DEBUG", "/var/log/myapp.log", true},
{"feature1", "feature2", "feature3"}
};
// Save to both formats
Common::saveToXMLFile(config, "config.xml", "AppConfig");
Common::saveToJSONFile(config, "config.json", "AppConfig");
// Load from either format
auto xmlConfig = Common::loadFromXMLFile<AppConfig>("config.xml", "AppConfig");
auto jsonConfig = Common::loadFromJSONFile<AppConfig>("config.json", "AppConfig");<AppConfig>
<AppName>MyApp</AppName>
<Version>1.0.0</Version>
<Database>
<Host>localhost</Host>
<Port>5432</Port>
<Username>admin</Username>
<Password>secret123</Password>
</Database>
<Logging>
<Level>DEBUG</Level>
<OutputPath>/var/log/myapp.log</OutputPath>
<Enabled>true</Enabled>
</Logging>
<Features>
<Feature>feature1</Feature>
<Feature>feature2</Feature>
<Feature>feature3</Feature>
</Features>
</AppConfig>{
"AppConfig": {
"AppName": "MyApp",
"Version": "1.0.0",
"Database": {
"Host": "localhost",
"Port": "5432",
"Username": "admin",
"Password": "secret123"
},
"Logging": {
"Level": "DEBUG",
"OutputPath": "/var/log/myapp.log",
"Enabled": "true"
},
"Features": {
"Feature": ["feature1", "feature2", "feature3"]
}
}
}- Keep serializers close to types: Define serialization operators in the same header as the struct
- Use const references: Pass structures by const reference in operator<<
- Template on NodeAdapter: This allows the same code to work with both XML and JSON
- Handle optional fields gracefully: Use std::optional for fields that may not be present
- Consistent naming: Use consistent XML/JSON element names across your project
- Test both formats: If you support both XML and JSON, test serialization/deserialization for both
- Once operators are defined, the type works with all FileParse features (vectors, maps, optional, etc.)
- The same serialization code works for both XML and JSON
- Nested structures serialize recursively
- Always test round-trip serialization (save and reload) to verify correctness