Reputation: 25310
I'm trying to deserialize an XML document in C#. The XML document comes form a Web API and the structure can't be changed. The document contains a list of items and each item can be one of four types. The type of each item is defined in a sub element of the class something like this (type names for sake of simplicity):
<?xml version="1.0" encoding="utf-8"?>
<items>
<item>
<type>Car</type>
<make>Ford</make>
<registration>AB00 AAA</registration>
</item>
<item>
<type>Bicycle</type>
<make>Specialized</make>
<frameSerialNo>123456768</frameSerialNo>
</item>
</items>
I want to deserialise this into a set of classes that inherit form an abstract class called Item like this:
abstract class Item
{
public string Make { get; set; }
}
class Bicycle : Item
{
public string FrameSerialNumber { get; set; }
}
class Car : Item
{
public string Registration { get; set; }
}
class ItemList
{
public Item[] Items { get; set; }
}
Is that possible using the System.Xml.Serialization.XmlSerializer class? If so what attributes should I set on my classes to make the inheritance part work?
Upvotes: 0
Views: 1012
Reputation: 14231
This cannot be done directly with XmlSerializer.
However, there are several ways to do it.
For example, you can create instances of classes and fill their properties manually. This preserves your structure of the classes. Only the array in the ItemList
class change to the List
for easy adding.
public class ItemList
{
public List<Item> Items { get; set; }
}
ItemList list = new ItemList();
list.Items = new List<Item>();
using (var reader = XmlReader.Create("test.xml"))
{
while (reader.ReadToFollowing("item"))
{
var inner = reader.ReadSubtree();
var item = XElement.Load(inner);
var type = item.Element("type");
if (type.Value == "Car")
{
var car = new Car();
car.Make = item.Element("make").Value;
car.Registration = item.Element("registration").Value;
list.Items.Add(car);
}
else if (type.Value == "Bicycle")
{
var bicycle = new Bicycle();
bicycle.Make = item.Element("make").Value;
bicycle.FrameSerialNumber = item.Element("frameSerialNo").Value;
list.Items.Add(bicycle);
}
}
}
However, if there many class properties and XML nodes respectively, it is quite tedious to manually write a lot of code.
In this case, you can deserialize each class separately. However, it is necessary to add XML attributes to our classes.
[XmlRoot("item")]
public abstract class Item
{
[XmlElement("make")]
public string Make { get; set; }
}
[XmlRoot("item")]
public class Bicycle : Item
{
[XmlElement("frameSerialNo")]
public string FrameSerialNumber { get; set; }
}
[XmlRoot("item")]
public class Car : Item
{
[XmlElement("registration")]
public string Registration { get; set; }
}
public class ItemList
{
public List<Item> Items { get; set; }
}
ItemList list = new ItemList();
list.Items = new List<Item>();
var carSerializer = new XmlSerializer(typeof(Car));
var bicycleSerializer = new XmlSerializer(typeof(Bicycle));
using (var reader = XmlReader.Create("test.xml"))
{
while (reader.ReadToFollowing("item"))
{
var inner = reader.ReadSubtree();
var item = XElement.Load(inner);
var type = item.Element("type");
if (type.Value == "Car")
{
var car = (Car)carSerializer.Deserialize(item.CreateReader());
list.Items.Add(car);
}
else if (type.Value == "Bicycle")
{
var bicycle = (Bicycle)bicycleSerializer.Deserialize(item.CreateReader());
list.Items.Add(bicycle);
}
}
}
Upvotes: 0
Reputation: 13384
Not directly, no.
You can either parse all the data manually with XmlDocument, XmlReader, etc. or feed a modified version of the XML to your XmlSerializer.
XmlSerializer would require a xsi:type attribute to be able to directly deserialize that XML. In your case that would look like this:
<Item xsi:type="Car">
Instead of
<Item>
<Type>Car</Type>
</Item>
If you can convert that structure before deserializing it (e.g. by manipulating an XmlDocument and then passing an XmlReader to the XmlSerializer instead of the original stream.
Example:
public static ItemList Load(Stream stream)
{
XmlDocument document = new XmlDocument();
document.Load(stream);
ModifyTypes(document);
XmlReader reader = new XmlNodeReader(document);
XmlSerializer serializer = new XmlSerializer(typeof(ItemList));
return serializer.Deserialize(reader) as ItemList;
}
public static ModifyTypes(XmlDocument document)
{
const string xsiNamespaceUri = "http://www.w3.org/2001/XMLSchema-instance";
XmlNodeList nodes = originalDocument.SelectNodes("//Item");
if (nodes == null) return;
foreach (XmlNode item in nodes)
{
if (item == null) continue;
if (item.Attributes == null) continue;
var typeAttribute = item.Attributes["type", xsiNamespaceUri];
if (typeAttribute != null) continue;
// here you'll have to add some logic to get the actual
// type name based on your structure
XmlAttribute attribute = document.CreateAttribute("xsi", "type", xsiNamespaceUri);
attribute.Value = "Car";
signDefinition.Attributes.Append(attribute);
}
}
Once you converted the data you have two options:
1.) Add an XmlInclude Attribute for each inherited class
[XmlInclude(typeof(Bicycle))]
[XmlInclude(typeof(Car))]
abstract class Item
2.) Explicitly specify all inherited types when serializing
XmlSerializer serializer = new XmlSerializer(typeof(ItemList), new[]{
typeof(Bicycle),
typeof(Car)
});
Another problem you will be facing is the fact, that your data structure is a bit different from your XML.
class ItemList
{
public Item[] Items { get; set; }
}
Serializing this ItemList would usually result in a structure similar to this:
<?xml version="1.0"?>
<ItemList xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Items>
<Item>...</Item>
<Item>...</Item>
<Item>...</Item>
</Items>
</ItemList>
So you might want to consider deserializing like this:
class ItemList
{
[XmlArray("Items")]
[XmlArrayItem("Item")]
public Item[] Items { get; set; }
public void Load(Stream stream)
{
//Insert Code options from above here
Items = serializer.Deserializer(typeof(Item[])) as Item[];
}
}
Upvotes: 2