XML serialization
Compiler support for [xml]
attributes and System.Xml.Serialization
library enable a program to write the value of a class object as an XML element, and read data back from XML to class object.
Single class
In the following code the Person class has an [xml]
attribute, so the compiler generates code to serialize the name
member variable as XML and set the value of the name
member variable from an XML element:
using System;
using System.Xml;
using System.Xml.Serialization;
[xml]
public class Person
{
public Person()
{
}
public Person(const string& name_) : name(name_)
{
}
public inline const string& Name() const
{
return name;
}
private string name;
}
int main()
{
return 0;
}
The following code creates a Person object and writes its data to an XML element named "person". Then the XML element is appended to an XML document and the document is written to console:
int main()
{
Person joe("Joe Coder");
XmlSerializationContext ctx;
ctx.SetFlag(XmlSerializationFlags.suppressMetadata);
Result<System.Xml.Element*> toXmlResult = joe.ToXml("person", ctx);
if (toXmlResult.Error())
{
Console.Error() << toXmlResult.GetErrorMessage() << "\n";
return 1;
}
System.Xml.Element* element = toXmlResult.Value();
System.Xml.Document document;
document.AppendChild(element);
System.Text.CodeFormatter formatter(Console.Out());
formatter.SetIndentSize(1);
Result<bool> writeResult = document.Write(formatter);
if (writeResult.Error())
{
Console.Error() << writeResult.GetErrorMessage() << "\n";
return 1;
}
}
The compiler implements a member function named ToXml
that takes care of writing. In this case I don't want any extra bookkeeping data to the XML so I have set the suppressMetadata
flag in the ToXml
call.
The output of the program will look like this:
<person>
<name value="Joe Coder"/>
</person>
You can set the name of the root XML element to whatever you like, in this case it's "person". The XML element for each member variable will have the same name as the member variable.
Reading data back into a Person object is not complicated either:
int main()
{
Person person;
Result<bool> fromXmlResult = person.FromXml(document.DocumentElement());
if (fromXmlResult.Error())
{
Console.Error() << fromXmlResult.GetErrorMessage() << "\n";
return 1;
}
Console.Out() << person.Name() << "\n";
}
The compiler implements a member function named FromXml
that reads data back from an XML element.
The code above prints the name of the Person object read, so the output will look like this:
Joe Coder
Here
is the source of the whole program.
Class with children
Suppose a class object will have child objects that we want to serialize also. In this case a Person may own vehicles. I have created an abstract Vehicle
class for them:
using System;
using System.Collections;
using System.Xml;
using System.Xml.Serialization;
public enum Color
{
white, black, red, blue
}
public string ColorStr(Color color)
{
switch (color)
{
case Color.white: return "white";
case Color.black: return "black";
case Color.red: return "red";
case Color.blue: return "blue";
}
return "<unknown color>";
}
[xml]
public abstract class Vehicle
{
public Vehicle()
{
}
public default virtual ~Vehicle();
public void SetModel(const string& model_)
{
model = model_;
}
public inline const string& Model() const
{
return model;
}
public void SetColor(Color color_)
{
color = color_;
}
public inline Color GetColor() const
{
return color;
}
public virtual void Print()
{
Console.Out() << ClassName() << "\n";
Console.Out() << "model: " << model << "\n";
Console.Out() << "color: " << ColorStr(color) << "\n";
}
private string model;
private Color color;
}
The Vehicle
class has an [xml]
attribute so its data will be serialized. The compiler supports also serializing enumeration values. In this case Color
is an enumerated type. Each XML serializable class will have a compiler implemented ClassName()
member function that returns the full name of the class. Here it is called in the virtual Print()
member function.
I have created two concrete XML serializable classes that derive from the abstract Vechicle
class, a Bicycle
class and a Car
class:
[xml]
public class Bicycle : Vehicle
{
public Bicycle()
{
}
public inline int Price()
{
return price;
}
public void SetPrice(int price_)
{
price = price_;
}
public override void Print()
{
base->Print();
Console.Out() << "price: " << price << "\n";
}
private int price;
}
[xml]
public class Car : Vehicle
{
public Car()
{
}
public void SetRegistrationNumber(const string& registrationNumber_)
{
registrationNumber = registrationNumber_;
}
public inline const string& RegistrationNumber() const
{
return registrationNumber;
}
public override void Print()
{
base->Print();
Console.Out() << "registrationNumber: " << registrationNumber << "\n";
}
private string registrationNumber;
}
An [xml]
-attributed class may derive from another [xml]
-attributed class or have no inheritance at all in which case the compiler will change it to derive from the abstract XmlSerializable
class. This is because the XmlSerializable
class will be then the ultimate base class of all XML serializable classes and the Cmajor language does not have multiple inheritance.
Now I have changed the Person
class to contain a list of UniquePtr
s to vehicles:
[xml]
public class Person
{
public Person()
{
}
public Person(const string& name_) : name(name_)
{
}
public inline const string& Name() const
{
return name;
}
public void AddVehicle(Vehicle* vehicle)
{
vehicles.Add(UniquePtr<Vehicle>(vehicle));
}
public void Print()
{
Console.Out() << "Person" << "\n";
Console.Out() << "name: " << name << "\n";
Console.Out() << "vehicles:" << "\n";
for (const auto& vehicle : vehicles)
{
vehicle->Print();
}
}
private string name;
private List<UniquePtr<Vehicle>> vehicles;
}
[nodiscard]
Result<bool> Register()
{
auto result = Person.Register();
if (result.Error()) return result;
result = Bicycle.Register();
if (result.Error()) return result;
result = Car.Register();
if (result.Error()) return result;
return Result<bool>(true);
}
An XML serializable class may have a UniquePtr
to another XML serializable class or a List
of UniquePtr
s to XML serializable classes that will be serialized.
When reading Person
data back from XML, the serialization library must be able to create concrete vehicle class objects when it sees a UniquePtr
to Vechicle
. Therefore now the serializable classes have been registered with the serialization library by calling the compiler-implemented Register()
member function of each XML serializable class in the Register()
function.
Here's now the code to serialize a person and vehicles it owns:
int main()
{
Result<bool> registerResult = Register();
if (registerResult.Error())
{
Console.Error() << registerResult.GetErrorMessage() << "\n";
return 1;
}
Person joe("Joe Coder");
Bicycle* cooper = new Bicycle();
cooper->SetModel("Cooper");
cooper->SetColor(Color.blue);
cooper->SetPrice(1000);
joe.AddVehicle(cooper);
Car* porsche = new Car();
porsche->SetModel("Porsche");
porsche->SetColor(Color.red);
porsche->SetRegistrationNumber("ABC-123");
joe.AddVehicle(porsche);
Result<System.Xml.Element*> toXmlResult = joe.ToXml("person");
if (toXmlResult.Error())
{
Console.Error() << toXmlResult.GetErrorMessage() << "\n";
return 1;
}
System.Xml.Element* element = toXmlResult.Value();
System.Xml.Document document;
document.AppendChild(element);
System.Text.CodeFormatter formatter(Console.Out());
formatter.SetIndentSize(1);
Result<bool> writeResult = document.Write(formatter);
if (writeResult.Error())
{
Console.Error() << writeResult.GetErrorMessage() << "\n";
return 1;
}
}
I have changed the ToXml
call to not to suppress XML metadata, so the program output will now look like this:
<person classId="2029169441" className="Person" objectId="eee6561b-4ab3-e38b-af80-9ba241b5295e">
<name value="Joe Coder"/>
<vehicles>
<item classId="2018315917" className="Bicycle" objectId="8035841f-fcd7-6da7-0182-745376e62d11">
<model value="Cooper"/>
<color value="3"/>
<price value="1000"/>
</item>
<item classId="737295636" className="Car" objectId="d607dda7-824e-ccda-2137-8afe117f8579">
<model value="Porsche"/>
<color value="2"/>
<registrationNumber value="ABC-123"/>
</item>
</vehicles>
</person>
The XML elements for the objects contain a classId
, className
and objectId
attributes. The classId
attributes enable the serialization library to create instances of corresponding serializable classes when needed. The objectId
attribute is not needed yet, because a Person
instance owns the Vechicle
s. The className
attribute is not needed by the library. It has only informative value for the person looking at the XML. When serializing a list of objects, the XML element name of them will be item
.
Here's the code to read person data back:
int main()
{
Person person;
Result<bool> fromXmlResult = person.FromXml(document.DocumentElement());
if (fromXmlResult.Error())
{
Console.Error() << fromXmlResult.GetErrorMessage() << "\n";
return 1;
}
person.Print();
return 0;
}
The Person.Print()
function will generate the following output:
Person
name: Joe Coder
vehicles:
Bicycle
model: Cooper
color: blue
price: 1000
Car
model: Porsche
color: red
registrationNumber: ABC-123
Here's
the whole program.
Network of objects
Suppose we have a network of XML serializable objects and we want to serialize them and their connections as a unit. In this case a Vehicle
keeps track of the Person
associated with it and Person
keeps track of vehicles associated with it but it does not own them. This is the new Vechicle
class:
using System;
using System.Collections;
using System.Xml;
using System.Xml.Serialization;
public enum Color
{
white, black, red, blue
}
public string ColorStr(Color color)
{
switch (color)
{
case Color.white: return "white";
case Color.black: return "black";
case Color.red: return "red";
case Color.blue: return "blue";
}
return "<unknown color>";
}
[xml]
public abstract class Vehicle
{
public Vehicle()
{
}
public default virtual ~Vehicle();
public void SetOwner(Person* owner_)
{
owner = owner_;
}
public void SetModel(const string& model_)
{
model = model_;
}
public inline const string& Model() const
{
return model;
}
public void SetColor(Color color_)
{
color = color_;
}
public inline Color GetColor() const
{
return color;
}
public virtual void Print()
{
Console.Out() << ClassName() << "\n";
if (!owner.IsNull())
{
Console.Out() << "owner: " << owner->Name() << "\n";
}
Console.Out() << "model: " << model << "\n";
Console.Out() << "color: " << ColorStr(color) << "\n";
}
private string model;
private Color color;
private System.Xml.Serialization.XmlPtr<Person> owner;
}
I have added an XmlPtr
to a Person
named owner
. An XmlPtr
is like an ordinary non-owning pointer, but it has to point to an XML serializable
object or be null, and it keeps track of the objectId
of the pointed-to object.
The vehicle classes have not changed, but this is the new Person
class:
[xml]
public class Person
{
public Person()
{
}
public Person(const string& name_) : name(name_)
{
}
public inline const string& Name() const
{
return name;
}
public void AddVehicle(Vehicle* vehicle)
{
vehicle->SetOwner(this);
vehicles.Add(System.Xml.Serialization.XmlPtr<Vehicle>(vehicle));
}
public void Print()
{
Console.Out() << "Person" << "\n";
Console.Out() << "name: " << name << "\n";
Console.Out() << "vehicles:" << "\n";
for (const auto& vehicle : vehicles)
{
vehicle->Print();
}
}
private string name;
private List<System.Xml.Serialization.XmlPtr<Vehicle>> vehicles;
}
[nodiscard]
Result<bool> Register()
{
auto result = Person.Register();
if (result.Error()) return result;
result = Bicycle.Register();
if (result.Error()) return result;
result = Car.Register();
if (result.Error()) return result;
return Result<bool>(true);
}
Now a person object does not own the vehicle objects, but has a list of XmlPtr
s to them.
The serialization code have been changed also:
int main()
{
auto registerResult = Register();
if (registerResult.Error())
{
Console.Error() << registerResult.GetErrorMessage() << "\n";
return 1;
}
System.Xml.Serialization.XmlBundle bundle;
Person* joe = new Person("Joe Coder");
bundle.Add(joe);
Bicycle* cooper = new Bicycle();
cooper->SetModel("Cooper");
cooper->SetColor(Color.blue);
cooper->SetPrice(1000);
joe->AddVehicle(cooper);
bundle.Add(cooper);
Car* porsche = new Car();
porsche->SetModel("Porsche");
porsche->SetColor(Color.red);
porsche->SetRegistrationNumber("ABC-123");
joe->AddVehicle(porsche);
bundle.Add(porsche);
Result<UniquePtr<System.Xml.Document>> xmlDocumentResult = bundle.ToXmlDocument();
if (xmlDocumentResult.Error())
{
Console.Error() << xmlDocumentResult.GetErrorMessage() << "\n";
return 1;
}
System.Xml.Document* document = xmlDocumentResult.Value().Get();
System.Text.CodeFormatter formatter(Console.Out());
formatter.SetIndentSize(1);
Result<bool> writeResult = document->Write(formatter);
if (writeResult.Error())
{
Console.Error() << writeResult.GetErrorMessage() << "\n";
return 1;
}
}
Here we have an XmlBundle
to which the serializable objects have been added to. By default an XmlBundle
takes ownership of the objects added to it.
The XmlBundle
class has a ToXmlDocument
member function, that creates an XML document, serializes the objects it contains as XML elements, adds them to the document, and returns the document.
Here's the XML document created by the bundle:
<xmlBundle>
<object classId="2029169441" className="Person" objectId="05c6a76c-b2c1-4cc6-bfc4-f266f8b76bd9">
<name value="Joe Coder"/>
<vehicles>
<item objectId="57cbe1dc-da35-39c5-2e74-799cc718529e"/>
<item objectId="438902f2-a4d7-d1a6-d0b0-777a8a7f6511"/>
</vehicles>
</object>
<object classId="2018315917" className="Bicycle" objectId="57cbe1dc-da35-39c5-2e74-799cc718529e">
<model value="Cooper"/>
<color value="3"/>
<owner objectId="05c6a76c-b2c1-4cc6-bfc4-f266f8b76bd9"/>
<price value="1000"/>
</object>
<object classId="737295636" className="Car" objectId="438902f2-a4d7-d1a6-d0b0-777a8a7f6511">
<model value="Porsche"/>
<color value="2"/>
<owner objectId="05c6a76c-b2c1-4cc6-bfc4-f266f8b76bd9"/>
<registrationNumber value="ABC-123"/>
</object>
</xmlBundle>
The object IDs enable the bundle in the reading side to restore object connections.
Here's the code to read a bundle back from XML:
int main()
{
System.Xml.Serialization.XmlBundle readBundle;
System.Xml.Serialization.XmlSerializationContext ctx;
ctx.SetFlag(System.Xml.Serialization.XmlSerializationFlags.failOnNotFoundObjects);
Result<bool> fromXmlResult = readBundle.FromXml(*document, ctx);
if (fromXmlResult.Error())
{
Console.Error() << fromXmlResult.GetErrorMessage() << "\n";
return 1;
}
if (readBundle.Count() != 3)
{
Console.Error() << "three objects expected" << "\n";
return 1;
}
System.Xml.Serialization.XmlSerializable* first = readBundle.Get(0);
if (first is Person*)
{
Person* person = cast<Person*>(first);
person->Print();
}
else
{
Console.Error() << "Person object expected" << "\n";
}
return 0;
}
The Person.Print()
function will generate the following output:
Person
name: Joe Coder
vehicles:
Bicycle
owner: Joe Coder
model: Cooper
color: blue
price: 1000
Car
owner: Joe Coder
model: Porsche
color: red
registrationNumber: ABC-123
Here's
the whole program.
Not serializing a member variable
If you don't want to serialize a member variable, declare the member variable with [xml="false"]
attribute:
using System;
using System.Xml;
using System.Xml.Serialization;
public class NotSerialized
{
}
[xml]
public class Foo
{
[xml="false"]
private NotSerialized* notSerialized;
}
void main()
{
}