How to deserialize an XML document into C# objects
Deserialisation turns an XML document into ordinary C# objects, so the rest of your code works with properties and lists instead of navigating nodes. XmlSerializer does it from attributes you put on your classes. Most problems come from a mismatch between the XML and those attributes, and the error message usually points straight at it.
The XML
<?xml version="1.0" encoding="utf-8"?>
<catalog updated="2026-10-01">
<book id="bk101" format="paperback">
<title>Patterns of Data Access</title>
<author>R. Moreau</author>
<price currency="EUR">32.50</price>
<tags>
<tag>ado.net</tag>
<tag>sql</tag>
</tags>
</book>
<book id="bk102">
<title>Forms on the Web</title>
<author>K. Ito</author>
<price currency="EUR">27.00</price>
</book>
</catalog>
The classes
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
[XmlRoot("catalog")]
public class Catalog
{
[XmlAttribute("updated", DataType = "date")]
public DateTime Updated { get; set; }
[XmlElement("book")] // repeated <book> elements directly under <catalog>
public List<Book> Books { get; set; } = new List<Book>();
}
public class Book
{
[XmlAttribute("id")]
public string Id { get; set; }
[XmlAttribute("format")]
public string Format { get; set; } // null when the attribute is absent
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("author")]
public string Author { get; set; }
[XmlElement("price")]
public Price Price { get; set; }
[XmlArray("tags")] // a wrapper element <tags>...
[XmlArrayItem("tag")] // ...containing <tag> items
public List<string> Tags { get; set; } = new List<string>();
}
public class Price
{
[XmlAttribute("currency")]
public string Currency { get; set; }
[XmlText] // the element's text content
public decimal Amount { get; set; }
}
Rules that XmlSerializer imposes: the classes must be public and have a public parameterless constructor, and only public read/write properties and fields are mapped. XML names are case-sensitive, which is why each attribute spells the element name exactly.
Notice the two kinds of list. [XmlElement] on a list maps repeated elements with no wrapper (book under catalog); [XmlArray] plus [XmlArrayItem] maps a wrapper element containing items (tags/tag). Using the wrong one gives an empty list rather than an error.
Deserialising
using System.IO;
using System.Xml;
using System.Xml.Serialization;
public static class CatalogReader
{
private static readonly XmlSerializer Serializer = new XmlSerializer(typeof(Catalog));
public static Catalog Load(Stream stream)
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit, // no DTDs, no external entities
XmlResolver = null,
IgnoreComments = true
};
using (XmlReader reader = XmlReader.Create(stream, settings))
{
return (Catalog)Serializer.Deserialize(reader);
}
}
public static Catalog LoadFile(string path)
{
using (FileStream fs = File.OpenRead(path)) return Load(fs);
}
public static Catalog LoadString(string xml)
{
using (var sr = new StringReader(xml))
using (XmlReader reader = XmlReader.Create(sr, new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }))
{
return (Catalog)Serializer.Deserialize(reader);
}
}
}
Creating an XmlSerializer generates and compiles code for the type, which is slow the first time. The XmlSerializer(Type) constructor caches that work internally, and keeping one instance in a static field, as here, is a common pattern. Avoid constructing serializers with the overloads that take extra types or attribute overrides in a loop: those are not cached and leak dynamically generated assemblies.
Using it in a Web Forms page
<asp:FileUpload ID="fuXml" runat="server" accept=".xml" />
<asp:Button ID="btnRead" runat="server" Text="Read catalogue" OnClick="btnRead_Click" />
<asp:Label ID="lblError" runat="server" />
<asp:GridView ID="gvBooks" runat="server" AutoGenerateColumns="false">
<Columns>
<asp:BoundField DataField="Id" HeaderText="Id" />
<asp:BoundField DataField="Title" HeaderText="Title" />
<asp:BoundField DataField="Author" HeaderText="Author" />
<asp:TemplateField HeaderText="Price">
<ItemTemplate><%#: Eval("Price.Amount", "{0:0.00}") %> <%#: Eval("Price.Currency") %></ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
protected void btnRead_Click(object sender, EventArgs e)
{
if (!fuXml.HasFile || fuXml.PostedFile.ContentLength > 1024 * 1024)
{
lblError.Text = "Choose an XML file of at most 1 MB.";
return;
}
try
{
Catalog catalog = CatalogReader.Load(fuXml.PostedFile.InputStream);
gvBooks.DataSource = catalog.Books;
gvBooks.DataBind();
}
catch (InvalidOperationException ex)
{
// XmlSerializer wraps parsing errors; the inner exception has the line and position
lblError.Text = Server.HtmlEncode(ex.InnerException?.Message ?? ex.Message);
}
}
Eval("Price.Amount") follows the property path into the nested object.
Namespaces
If the root element declares a default namespace, every element in it belongs to that namespace, and the classes must say so:
<catalog xmlns="urn:example:catalog"> ... </catalog>
[XmlRoot("catalog", Namespace = "urn:example:catalog")]
public class Catalog
{
[XmlElement("book", Namespace = "urn:example:catalog")]
public List<Book> Books { get; set; }
}
// Child classes' elements inherit the namespace of their parent element by default.
The error "<catalog xmlns='urn:example:catalog'> was not expected" means exactly this: the root element's name or namespace does not match [XmlRoot].
Generating classes instead of writing them
- With an XSD schema, run
xsd.exe schema.xsd /classesfrom a Developer Command Prompt to generate the classes. - Without a schema, Visual Studio's Edit › Paste Special › Paste XML As Classes creates classes from a sample document. Review them: they reflect only what the sample contains.
Common errors
| Message | Cause |
|---|---|
| There is an error in XML document (1, 2). <x> was not expected. | Root name or namespace differs from [XmlRoot]. |
| X cannot be serialized because it does not have a parameterless constructor. | Add a public parameterless constructor. |
| Input string was not in a correct format (inner exception) | A number or date in the XML is not in XML Schema format (for example 32,50 instead of 32.50). XML uses invariant formats regardless of server culture. |
| Properties stay null | Name mismatch (case), missing namespace, or XmlElement/XmlArray mix-up. |
Security with untrusted XML
XML from users or partners can carry entity expansion attacks ("billion laughs") or external entity references that read local files. Reading through an XmlReader with DtdProcessing.Prohibit and no resolver, as above, blocks both. On .NET Framework 4.5.2 and later the defaults are already safer, but stating the settings documents the intent. Limit the size of what you accept, as the page does.
Alternatives
For quick, read-only access to a few values, LINQ to XML (XDocument) is often simpler than mapping classes: doc.Root.Elements("book").Select(b => (string)b.Element("title")). For JSON instead of XML, System.Text.Json plays the same role as XmlSerializer.
Official documentation
- Microsoft Learn: XmlSerializer class and Controlling XML serialization using attributes
- Microsoft Learn: XML Schema Definition tool (xsd.exe)
- Microsoft Learn: XmlReaderSettings.DtdProcessing
- OWASP: XML External Entity Prevention Cheat Sheet