XML Serialization: handling a collection of different classes


For some time now, I have had the need to serialize and deserialize XML that contains an element that is defined as a collection of a set of derived classes. Below is a contrived example:

<Orchestra>
<Instruments>
  <Brass>trumpet</Brass>
<Brass>trombone</Brass>
<Woodwind>oboe</Woodwind>
<Brass>cornet</Brass>
<Woodwind>clarinet</Woodwind>
</Instruments>
</Orchestra>

In this example, Brass and Woodwind are supposed to be classes derived from an Instrument class, and Instruments is a collection of Instrument items.

But how would one map this to a set of classes in C#?

[XmlRoot] 
public class Orchestra
{
    [XmlArray]
    [XmlArrayItem("Instrument")]
    public List<Instrument> Instruments { get; set; }
}

public class Instrument
{
   [XmlText]
    public string Name { get; set; }
}

public class Brass : Instrument { }

public class Woodwind : Instrument { }

This seems like a good start, but if one uses an XmlSerializer to deserialize the previous XML, it will return an Orchestra object where the Instruments member is null.

The XmlSerializer does not honour the fact that Brass and Woodwind are both derived from Instrument.

One possibility would be to change Instruments to be a class that has a collection of Brass objects and a collection of Woodwind objects. However, for my purposes, I needed to keep the items in a single collection, so forcing them into separate collections only made my life more difficult.

So, I thought that maybe I could make Brass and Woodwind both implementations of an IInstrument interface and declare Instruments to be a list of IInstruments.

No luck: C# does not allow serialization/deserialization of interfaces.

So, for the longest while, I was stuck with something akin to this bit of nastiness:

<Orchestra>
<Instruments>
  <Instrument type="Brass" name="trumpet" />
<Instrument type="Brass" name="trombone" />
<Instrument type="Woodwind" name="oboe" />
<Instrument type="Brass" name="cornet" />
<Instrument type="Woodwind" name="clarinet" />
</Instruments>
</Orchestra>

Besides being a complete abuse of the concept of XML attributes, it resulted in a lot of ugly code. I was never satisfied with this format as a workaround – but I didn’t see an alternative.

Until I found this example. (note: before delving into the code below, be aware that there is a much simpler method discussed later, in the addendum section at the bottom of this post)

By following methods used in the code in the link, a new class can be constructed:

private static XmlSerializer CreateCustomSerializer() 
{
// Create overrides that allow each Brass or Woodwind object 
// to be read from and written as members of an Instruments
// collection.
// Oddly enough, an override is also needed to allow an      // Instrument to be read/written as an Instrument.     XmlAttributes xAttrs = new XmlAttributes();     xAttrs.XmlArrayItems.Add(new XmlArrayItemAttribute(typeof(Brass)));     xAttrs.XmlArrayItems.Add(new XmlArrayItemAttribute(typeof(Woodwind)));     xAttrs.XmlArrayItems.Add(new XmlArrayItemAttribute(typeof(Instrument)));

var overrides = new XmlAttributeOverrides();     overrides.Add(typeof(Orchestra), "Instruments", xAttrs);

    var serializer = new XmlSerializer(typeof(Orchestra), overrides);
    return serializer;
}

One can then serialize and deserialize with

var serializer = CreateCustomSerializer(); 
var writer = new StringWriter();
serializer.Serialize(writer, from);
return writer.ToString();

XmlSerializer serializer = CreateCustomSerializer();
var reader = new StringReader(text);
return (Orchestra)serializer.Deserialize(reader);

Here is a full, working example:

using System; 
using System.Collections.Generic; 
using System.IO; 
using System.Xml.Serialization;   

namespace XmlOverrideTest 
{
  [XmlRoot]
  public class Orchestra
  {
     [XmlArray]
     [XmlArrayItem("Instrument")] 
     public List<Instrument> Instruments { get; set; }
  }

  public class Instrument
  {
     [XmlText]
     public string Name { get; set; }     
  }

  public class Brass : Instrument { }

  public class Woodwind : Instrument { } 

  class Program
  {
    static void Main(string[] args)
    {
      var required =
        "<Orchestra>" +
          "<Instruments>" + 
            "<Instrument>generic</Instrument>" + 
            "<Brass>trumpet</Brass>" + 
            "<Brass>trombone</Brass>" + 
            "<Woodwind>oboe</Woodwind>" + 
            "<Brass>cornet</Brass>" + 
            "<Woodwind>clarinet</Woodwind>" + 
          "</Instruments>" +
        "</Orchestra>"; 
     
      // create an object representing the orchestra in the XML:
      var pops = FromXml(required);
      // change the object back into XML text:
      var toText = FromOrchestra(pops);
    }   

    private static string FromOrchestra(Orchestra from)
    {
      var serializer = CreateCustomSerializer(); 
      var writer = new StringWriter(); 
      serializer.Serialize(writer, from); 
      return writer.ToString();
    }

    private static Orchestra FromXml(string text)
    { 
      var serializer = CreateCustomSerializer();
      var reader = new StringReader(text); 
      return (Orchestra)serializer.Deserialize(reader);
    }   

    private static XmlSerializer CreateCustomSerializer()
    { 
      // Create overrides that allow each Brass or Woodwind object
      // to be read from and written as members of an Instruments
      // collection.  
      // Oddly enough, an override is also needed to allow an
      // Instrument to be read/written as an Instrument. 
      var xAttrs = new XmlAttributes(); 
      xAttrs.XmlArrayItems.Add(
        new XmlArrayItemAttribute(typeof(Brass)));
      xAttrs.XmlArrayItems.Add(
        new XmlArrayItemAttribute(typeof(Woodwind)));  
      xAttrs.XmlArrayItems.Add(
        new XmlArrayItemAttribute(typeof(Instrument)));
  
      var overrides = new XmlAttributeOverrides(); 
      overrides.Add(typeof(Orchestra), "Instruments", xAttrs);

      var serializer = 
        new XmlSerializer(typeof(Orchestra), overrides);
      return serializer;
    }
  }
} 

I don’t know why the override is needed to get an Instrument to be treated as an Instrument, but at least I know it works.


Addendum:

I have since found out that it is possible to achieve the same results using a different method: by using the Type value of the XmlArrayItem attribute (see https://docs.microsoft.com/en-us/dotnet/standard/serialization/controlling-xml-serialization-using-attributes).

Compare the above to this, much cleaner, simpler code:

using System; 
using System.Collections.Generic; 
using System.IO; 
using System.Xml.Serialization;   

namespace XmlOverrideTest 
{
  [XmlRoot]
  public class Orchestra
  {
     [XmlArray]
     [XmlArrayItem("Instrument", Type = typeof(Instrument)),
          XmlArrayItem(Type = typeof(Brass)),
          XmlArrayItem(Type = typeof(Woodwind))] 
     public List<Instrument> Instruments { get; set; }
  }

  public class Instrument
  {
     [XmlText]
     public string Name { get; set; }     
  }

  public class Brass : Instrument { }

  public class Woodwind : Instrument { } 

  class Program
  {
    static void Main(string[] args)
    {
      var required =
        "<Orchestra>" +
          "<Instruments>" + 
            "<Instrument>generic</Instrument>" + 
            "<Brass>trumpet</Brass>" + 
            "<Brass>trombone</Brass>" + 
            "<Woodwind>oboe</Woodwind>" + 
            "<Brass>cornet</Brass>" + 
            "<Woodwind>clarinet</Woodwind>" + 
          "</Instruments>" +
        "</Orchestra>"; 
     
      // create an object representing the orchestra in the XML:
      var pops = FromXml(required);
      // change the object back into XML text:
      var toText = FromOrchestra(pops);
    }   

    private static string FromOrchestra(Orchestra from)
    {
      var serializer = new XmlSerializer(typeof(Orchestra)); 
      var writer = new StringWriter(); 
      serializer.Serialize(writer, from); 
      return writer.ToString();
    }

    private static Orchestra FromXml(string text)
    { 
      var serializer = new XmlSerializer(typeof(Orchestra));
      var reader = new StringReader(text); 
      return (Orchestra)serializer.Deserialize(reader);
    }   
  }
}  

Both methods have their places though. While the second method is cleaner, it requires a greater level of foreknowledge of the data structure. Whereas, the first method can be much more easily adapted to adding new classes to the collection at run-time.

Leave a comment