Updated on

XML serialization turns the public fields and properties of an object into an XML document. In C# we do it with XmlSerializer from the System.Xml.Serialization namespace: we construct one for the type, then call Serialize() with a destination and the object.

The object decides the shape of the document. Property names become element names, nested objects become nested elements, and attributes such as [XmlRoot], [XmlAttribute] and [XmlIgnore] change that mapping when we need a different document.

To download the source code for this article, you can visit our GitHub repository.

How Do We Serialize an Object to XML in C#?

Serializing an object to XML in C# takes three steps.

First, we construct an XmlSerializer for the type we want to write, passing typeof(Patient). The serializer inspects that type and builds a map between its members and XML nodes.

Then we call Serialize(), passing the destination and the object. The destination is the choice: a Stream, a TextWriter such as StreamWriter, or an XmlWriter when we want control over indentation and the XML declaration.

Finally, we dispose the writer, or wrap it in a using statement, so the file is closed. Serialize() flushes what it wrote before it returns, but the file stays open until the writer is disposed.

Two rules hold for every example that follows. The type needs a parameterless constructor, and only public members are written.

The constructor we call matters as well. Only the XmlSerializer(Type) and XmlSerializer(Type, String) constructors reuse the assembly the serializer generates. The other overloads generate a fresh one on every construction and never unload it.

That last rule comes from the Dynamically generated assemblies section of the XmlSerializer class reference. Our deserialization article explains which XmlSerializer constructors cache the assembly they generate and how to cache the others ourselves.

We are going to start with the serialization of a simple C# class:

public class Patient
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime Birthday { get; set; }
    public int RoomNo { get; set; }
}

The Patient class contains the necessary information for a hospital patient.

Next, let’s create a new object of type Patient. We are going to serialize it into an XML file using the XmlSerializer class:

using System.Xml.Serialization;

var patient = new Patient()
{
    ID = 232323,
    FirstName = "John",
    LastName = "Doe",
    Birthday = new DateTime(1990, 12, 30),
    RoomNo = 310
};

var serializer = new XmlSerializer(typeof(Patient));
using (var writer = new StreamWriter("patients.xml"))
{
    serializer.Serialize(writer, patient);
}

Here, we create an XmlSerializer object that will serialize objects of type Patient. The Serialize() method transforms the object into XML. It also uses a StreamWriter object to write the XML into a file.

The resulting XML file consists of an XML Root Element and all the information of the Patient object:

<?xml version="1.0" encoding="utf-8"?>
<Patient xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <ID>232323</ID>
  <FirstName>John</FirstName>
  <LastName>Doe</LastName>
  <Birthday>1990-12-30T00:00:00</Birthday>
  <RoomNo>310</RoomNo>
</Patient>

Note that the tags of the XML elements have exactly the same name as the properties of the Patient object. We can change their names with the use of C# Attributes.

Things to Note About Serialization in C#

There are some important details about XML serialization in C# that we should be aware of. First of all, every class that we want to serialize should define the default (parameterless) constructor. In our case, we have not defined any constructors. Therefore, the parameterless constructor is included by default.

Moreover, only the public members of a class will be serialized; private members will be omitted from the XML document.

A property is serialized when it has a public getter and a public setter. A property with no setter is skipped without an error, unless it holds a collection the serializer can fill, such as a List<T>. A property whose setter is private throws when the serializer is constructed.

What Does XmlSerializer Leave Out of the XML?

XmlSerializer writes public fields, unless they are readonly, and public properties that have both a getter and a setter. Every other member is either dropped without a word or rejected with an exception.

Dropped in silence: private and internal members, a property with a getter and no setter, an expression-bodied property, and any reference-type property whose value is null. A null property does not produce an empty element. The element is absent.

Rejected outright: a property with a public getter and a private setter, a type without a parameterless constructor, and any member typed as a dictionary. Each throws when the serializer is constructed, before a single character is written.

Read-only collections are the exception. A List<T> property with a getter and no setter is written normally, because when the serializer reads the document back it fills the existing list instead of assigning a new one.

The table below lists the common cases, and every row in it comes from running the code on .NET 10:

Member shapeIn the XML?What actually happens
public int X { get; set; }YesWritten as an element named X
public int X; (public field)YesWritten, and fields come before properties in the output
public int X { get; } (no setter)NoSkipped without an error
public int X => 44; (expression-bodied)NoSkipped without an error
public int X { get; private set; }ThrowsInvalidOperationException when the serializer is constructed
public List<string> X { get; } (no setter)YesWritten; the serializer fills the existing list, so no setter is needed
internal or private memberNoSkipped
public string? X { get; set; } set to nullNoThe element is omitted entirely
public Dictionary<K,V> X { get; set; }ThrowsInvalidOperationException wrapping a NotSupportedException, "because it implements IDictionary"
A class with no parameterless constructorThrowsInvalidOperationException when the serializer is constructed
A class whose parameterless constructor is privateYesWritten; the constructor does not have to be public
A member marked [Obsolete("...", true)]ThrowsInvalidOperationException, "Cannot serialize member with [Obsolete(IsError=true)]"
A member or type marked plain [Obsolete]YesWritten normally, although the documentation says otherwise

The private-setter row comes with a confusing message. If we give RoomNo a private setter, constructing the serializer for Patient throws:

System.InvalidOperationException: Cannot deserialize type 'Patient' because it contains property 'RoomNo' which has no public setter.

The message says “deserialize” although we only asked to serialize. To fix it, we make the setter public, or mark the property [XmlIgnore] if it should stay out of the document.

The two [Obsolete] rows disagree with the XmlSerializer class reference, which says the class “does not serialize objects that are marked as [Obsolete]”. On .NET 10.0.10, a plain [Obsolete] member or type is written like any other, and only the error form, [Obsolete("...", true)], stops the serializer when it is constructed. To keep a member out of the document, we use [XmlIgnore].

Serializing Classes with Nested Classes to XML

We can also serialize classes that contain objects from other classes.

Let’s introduce a new class that contains information about the patient’s home address:

public class Address
{
    public string Street { get; set; }
    public string Zip { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

Next, we are going to modify the Patient with a reference to Address:

public class Patient
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime Birthday { get; set; }
    public int RoomNo { get; set; }
    public Address HomeAddress { get; set; }
}

To serialize the patient object, we are going to use the XmlSerializer class:

using System.Xml.Serialization;

var patient = new Patient()
{
    ID = 232323,
    FirstName = "John",
    LastName = "Doe",
    Birthday = new DateTime(1990, 12, 30),
    RoomNo = 310,
    HomeAddress = new Address()
    {
        Street = "12 Main str.",
        Zip = "23322",
        City = "Boston",
        Country = "USA",
    }
};

var serializer = new XmlSerializer(typeof(Patient));
using (var writer = new StreamWriter("patients.xml"))
{
    serializer.Serialize(writer, patient);
}

The resulting XML is going to have an additional element with the patient’s address details:

<?xml version="1.0" encoding="utf-8"?>
<Patient xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <ID>232323</ID>
  <FirstName>John</FirstName>
  <LastName>Doe</LastName>
  <Birthday>1990-12-30T00:00:00</Birthday>
  <RoomNo>310</RoomNo>
  <HomeAddress>
    <Street>12 Main str.</Street>
    <Zip>23322</Zip>
    <City>Boston</City>
    <Country>USA</Country>
  </HomeAddress>
</Patient>

How Do We Serialize an Array of Objects to XML?

In order to serialize an array of objects, we have to instruct XmlSerializer to expect objects of type Patient[]:

using System.Xml.Serialization;

var arr = new Patient[]
{
    new Patient() { ID = 232323, FirstName = "John", LastName = "Doe" },
    new Patient() { ID = 454545, FirstName = "Jane", LastName = "Doe" }
};

var arrSerializer = new XmlSerializer(typeof(Patient[]));
using (var writer = new StreamWriter("patients.xml"))
{
    arrSerializer.Serialize(writer, arr);
}

The resulting XML now contains a Root Element with the name ArrayOfPatient:

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfPatient xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Patient>
    ...
  </Patient>
  <Patient>
    ...
  </Patient>
</ArrayOfPatient>

Serializing Classes with Lists of Objects to XML

The XmlSerializer class can also serialize classes that contain lists (or arrays) of objects:

public class Patient
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime Birthday { get; set; }
    public int RoomNo { get; set; }
    public Address HomeAddress { get; set; }
    public List<Measurement> Measurements {get; set;}
}

Each Measurement stores a temperature reading for the patient:

public class Measurement
{
    public DateTime TimeTaken { get; set; }
    public decimal Temp { get; set; }
}

We can serialize a Patient object in the same way as before:

using System.Xml.Serialization;

var patient = new Patient()
{
    ID = 232323,
    FirstName = "John",
    LastName = "Doe",
    Birthday = new DateTime(1990, 12, 30),
    RoomNo = 310,
    HomeAddress = new Address()
    {
        Street = "12 Main str.",
        Zip = "23322",
        City = "Boston",
        Country = "USA",
    },
    Measurements = new List<Measurement>()
    {
        new Measurement() { TimeTaken = new DateTime(2022, 1, 1, 17, 50, 0), Temp = 38.4M },
        new Measurement() { TimeTaken = new DateTime(2022, 1, 1, 22, 00, 0), Temp = 39.1M },
        new Measurement() { TimeTaken = new DateTime(2022, 1, 2, 6, 30, 0), Temp = 37.3M }
    }
};

var serializer = new XmlSerializer(typeof(Patient));
using (var writer = new StreamWriter("patients.xml"))
{
    serializer.Serialize(writer, patient);
}

Our XML file will now have an array of elements with the corresponding measurements:

<?xml version="1.0" encoding="utf-8"?>
<Patient xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  ...
  <Measurements>
    <Measurement>
      <TimeTaken>2022-01-01T17:50:00</TimeTaken>
      <Temp>38.4</Temp>
    </Measurement>
    <Measurement>
      <TimeTaken>2022-01-01T22:00:00</TimeTaken>
      <Temp>39.1</Temp>
    </Measurement>
    <Measurement>
      <TimeTaken>2022-01-02T06:30:00</TimeTaken>
      <Temp>37.3</Temp>
    </Measurement>
  </Measurements>
</Patient>

How Do We Control the XML Output With C# Attributes?

Attributes from System.Xml.Serialization change the mapping between a class and its document. The class itself keeps the same properties, so the rest of the application uses it unchanged.

[XmlRoot("PatientInfo")] renames the document element. [XmlElement("RoomNumber")] renames one element. [XmlAttribute("PatientID")] moves a value out of its own element and onto the object’s element as an XML attribute.

[XmlIgnore] removes a member from the document entirely, which is how we keep a computed or sensitive property out of a file we hand to someone else.

Collections take a pair of attributes. [XmlArray("TemperatureReadings")] names the wrapper element and [XmlArrayItem("TemperatureReading")] names each item inside it, so the element names in the document no longer have to match the Measurements property and the Measurement class.

[XmlText] writes a value as the text content of its parent element instead of as a child element.

One more attribute from the same namespace, [XmlInclude], lists the derived types a base class may hold, and we need it in the derived-classes section further down.

Here is how the attributes we are about to add map the Patient class onto its document:

A C# Patient class on the left mapped to its XML document on the right: XmlRoot renames the root element to PatientInfo, XmlAttribute turns ID into a PatientID attribute, XmlElement renames RoomNo to RoomNumber, FirstName keeps its name, and XmlIgnore leaves Birthday out with no element.

So, let’s start with the Patient class modification by adding the required attributes:

using System.Xml.Serialization;

[XmlRoot("PatientInfo")]
public class Patient
{
    [XmlAttribute ("PatientID")]
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    [XmlIgnore]
    public DateTime Birthday { get; set; }
    [XmlElement("RoomNumber")]
    public int RoomNo { get; set; }
    public Address HomeAddress { get; set; }
    [XmlArray("TemperatureReadings")]
    [XmlArrayItem("TemperatureReading")]
    public List<Measurement> Measurements {get; set;}
}

First of all, we can change the name of the Root Element from Patient to PatientInfo, using of XmlRoot annotation.

Next, we choose to serialize the ID property as an XmlAttribute, under the name PatientID.

The XmlIgnore annotation prevents a property from appearing in the XML. Here, we use it to omit the patient’s birthday from the resulting file.

Furthermore, when our object contains a list – or an array – of objects, we can use the XmlArray and XmlArrayItem attributes to modify the way they will be serialized. The XmlArray attribute changes the array’s tag name from Measurements to TemperatureReadings. Also, XmlArrayItem provides a new name for each element of the Measurements array.

Finally, let’s add the XmlText attribute to the Address class:

using System.Xml.Serialization;

public class Address
{
    public string Street { get; set; }
    public string Zip { get; set; }
    public string City { get; set; }
    [XmlText]
    public string Country { get; set; }
}

With the use of XmlText, we are going to serialize the Country property as plain text inside the HomeAddress element.

As a result, we will have a different XML file:

<?xml version="1.0" encoding="utf-8"?>
<PatientInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" PatientID="232323">
  <FirstName>John</FirstName>
  <LastName>Doe</LastName>
  <RoomNumber>310</RoomNumber>
  <HomeAddress>
    <Street>12 Main str.</Street>
    <Zip>23322</Zip>
    <City>Boston</City>USA</HomeAddress>
  <TemperatureReadings>
    <TemperatureReading>
      <TimeTaken>2022-01-01T17:50:00</TimeTaken>
      <Temp>38.4</Temp>
    </TemperatureReading>
    <TemperatureReading>
      <TimeTaken>2022-01-01T22:00:00</TimeTaken>
      <Temp>39.1</Temp>
    </TemperatureReading>
    <TemperatureReading>
      <TimeTaken>2022-01-02T06:30:00</TimeTaken>
      <Temp>37.3</Temp>
    </TemperatureReading>
  </TemperatureReadings>
</PatientInfo>

The serializer writes USA directly after the City element and closes HomeAddress on the same line. Once an element holds text of its own, the writer stops indenting inside it until that element closes.

Handling Derived Classes

A special case of the list (or array) serialization arises when a list contains references to objects of derived classes. This usually happens with polymorphism. Let’s suppose we have a Person class:

public class Person
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime Birthday { get; set; }
}

This class serves as the base for the creation of the Patient class:

public class Patient: Person
{
    public int RoomNo { get; set; }
}

And the Doctor class:

public class Doctor: Person
{
    public string Specialization { get; set; }
}

Now, let’s create a list that contains references to objects of both types:

using System.Xml.Serialization;

var people = new List<Person>()
{
    new Doctor()
    {
        ID = 777,
        FirstName = "Jane",
        LastName = "Doe",
        Birthday = new DateTime(1975, 3, 5),
        Specialization = "Cardiologist"
    },
    new Patient()
    {
        ID = 888,
        FirstName = "John",
        LastName = "Doe",
        Birthday = new DateTime(1980, 3, 21),
        RoomNo = 301
    }
};

var serializer = new XmlSerializer(typeof(List<Person>));
using (TextWriter writer = new StreamWriter("patients.xml"))
{
    serializer.Serialize(writer, people);
}

If we try to serialize this list, we will get an InvalidOperationException:

Unhandled exception. System.InvalidOperationException: There was an error generating the XML document.
 ---> System.InvalidOperationException: The type Doctor was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically.

As the exception detail hints, we should specify the derived types using the XmlInclude attribute:

using System.Xml.Serialization;

[XmlInclude(typeof(Doctor))]
[XmlInclude(typeof(Patient))]
public class Person
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime Birthday { get; set; }
}

In this case, the serialization completes successfully. The resulting XML contains both objects with their respective content:

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfPerson xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Person xsi:type="Doctor">
    <ID>777</ID>
    <FirstName>Jane</FirstName>
    <LastName>Doe</LastName>
    <Birthday>1975-03-05T00:00:00</Birthday>
    <Specialization>Cardiologist</Specialization>
  </Person>
  <Person xsi:type="Patient">
    <ID>888</ID>
    <FirstName>John</FirstName>
    <LastName>Doe</LastName>
    <Birthday>1980-03-21T00:00:00</Birthday>
    <RoomNo>301</RoomNo>
  </Person>
</ArrayOfPerson>

How Do We Control Indentation, Encoding and Namespaces?

Three details of every output so far come from the serializer and the writer, and our class has no say in them: the namespace declarations on the root element, the encoding in the XML declaration, and the indentation.

The xmlns:xsi and xmlns:xsd declarations on the root element come from the serializer. Passing an XmlSerializerNamespaces that contains a single empty prefix and empty namespace removes both.

The declared encoding follows the writer we chose. Serializing to a StringWriter produces encoding="utf-16", because a .NET string is UTF-16, even though the file we later save may use any encoding.

Indentation, the declaration itself and the real encoding are XmlWriterSettings properties. We create an XmlWriter over a file or a stream with Indent, OmitXmlDeclaration and Encoding set, and pass that writer to Serialize().

As a rule of thumb, we write to a Stream or an XmlWriter when the bytes matter, and keep StringWriter for the cases where we only want to look at the document.

Here is the patient from the first example again, written through an XmlWriter with indentation, UTF-8 and no namespace declarations:

using System.Text;
using System.Xml;
using System.Xml.Serialization;

var patient = new Patient()
{
    ID = 232323,
    FirstName = "John",
    LastName = "Doe",
    Birthday = new DateTime(1990, 12, 30),
    RoomNo = 310
};

var namespaces = new XmlSerializerNamespaces();
namespaces.Add(string.Empty, string.Empty);

var settings = new XmlWriterSettings
{
    Indent = true,
    Encoding = new UTF8Encoding(false)
};

var serializer = new XmlSerializer(typeof(Patient));
using var writer = XmlWriter.Create("patient.xml", settings);
serializer.Serialize(writer, patient, namespaces);

The file now starts with a utf-8 declaration and a bare root element:

<?xml version="1.0" encoding="utf-8"?>
<Patient>
  <ID>232323</ID>
  <FirstName>John</FirstName>
  <LastName>Doe</LastName>
  <Birthday>1990-12-30T00:00:00</Birthday>
  <RoomNo>310</RoomNo>
</Patient>

Passing new UTF8Encoding(false) also keeps the byte order mark out of the file. With the default XmlWriterSettings, the writer puts one in front of the declaration.

Conclusion

In this post, we have seen how to perform XML serialization in C#. More specifically, we have learned to serialize simple and complex objects, as well as arrays and lists of objects. We have also learned which members the serializer leaves out, how to alter the structure of the resulting XML file with the use of attributes, and how the writer controls indentation, encoding and namespaces.

To go the other way, we can turn an XML document back into a C# object. When we want to work with a document directly instead of through a class, we can read and parse an XML file or build an XML document from scratch.

Tested with .NET 10.0.10.