.NET XML serialization gotchas?

Viewed 67445

I've run into a few gotchas when doing C# XML serialization that I thought I'd share:


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

[XmlRoot("dictionary")]
public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
{      
    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(System.Xml.XmlReader reader)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

        bool wasEmpty = reader.IsEmptyElement;
        reader.Read();

        if (wasEmpty)
            return;

        while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
        {
            reader.ReadStartElement("item");

            reader.ReadStartElement("key");
            TKey key = (TKey)keySerializer.Deserialize(reader);
            reader.ReadEndElement();

            reader.ReadStartElement("value");
            TValue value = (TValue)valueSerializer.Deserialize(reader);
            reader.ReadEndElement();

            this.Add(key, value);

            reader.ReadEndElement();
            reader.MoveToContent();
        }
        reader.ReadEndElement();
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

        foreach (TKey key in this.Keys)
        {
            writer.WriteStartElement("item");

            writer.WriteStartElement("key");
            keySerializer.Serialize(writer, key);
            writer.WriteEndElement();

            writer.WriteStartElement("value");
            TValue value = this[key];
            valueSerializer.Serialize(writer, value);
            writer.WriteEndElement();

            writer.WriteEndElement();
        }
    }
}

Any other XML Serialization gotchas out there?

19 Answers

Another huge gotcha: when outputting XML through a web page (ASP.NET), you don't want to include the Unicode Byte-Order Mark. Of course, the ways to use or not use the BOM are almost the same:

BAD (includes BOM):

XmlTextWriter wr = new XmlTextWriter(stream, new System.Text.Encoding.UTF8);

GOOD:

XmlTextWriter  wr = new XmlTextWriter(stream, new System.Text.UTF8Encoding(false))

You can explicitly pass false to indicate you don't want the BOM. Notice the clear, obvious difference between Encoding.UTF8 and UTF8Encoding.

The three extra BOM Bytes at the beginning are (0xEFBBBF) or (239 187 191).

Reference: http://chrislaco.com/blog/troubleshooting-common-problems-with-the-xmlserializer/

I can't make comments yet, so I will comment on Dr8k's post and make another observation. Private variables that are exposed as public getter/setter properties, and do get serialized/deserialized as such through those properties. We did it at my old job al the time.

One thing to note though is that if you have any logic in those properties, the logic is run, so sometimes, the order of serialization actually matters. The members are implicitly ordered by how they are ordered in the code, but there are no guarantees, especially when you are inheriting another object. Explicitly ordering them is a pain in the rear.

I've been burnt by this in the past.

If the serializer encounters a member/property that has an interface as its type, it won't serialize. For example, the following won't serialize to XML:

public class ValuePair
{
    public ICompareable Value1 { get; set; }
    public ICompareable Value2 { get; set; }
}

Though this will serialize:

public class ValuePair
{
    public object Value1 { get; set; }
    public object Value2 { get; set; }
}

IEnumerables<T> that are generated via yield returns are not serializable. This is because the compiler generates a separate class to implement yield return and that class is not marked as serializable.

You can't serialize read-only properties. You must have a getter and a setter, even if you never intend to use deserialization to turn XML into an object.

For the same reason, you can't serialize properties that return interfaces: the deserializer wouldn't know what concrete class to instantiate.

Oh here's a good one: since the XML serialization code is generated and placed in a separate DLL, you don't get any meaningful error when there is a mistake in your code that breaks the serializer. Just something like "unable to locate s3d3fsdf.dll". Nice.

Can't serialize an object which doesn't have a parameterless construtor (just got bitten by that one).

And for some reason, from the following properties, Value gets serialised, but not FullName:

    public string FullName { get; set; }
    public double Value { get; set; }

I never got round to working out why, I just changed Value to internal...

If your XML Serialization generated assembly is not in the same Load context as the code attempting to use it, you will run into awesome errors like:

System.InvalidOperationException: There was an error generating the XML document.
---System.InvalidCastException: Unable to cast object
of type 'MyNamespace.Settings' to type 'MyNamespace.Settings'. at
Microsoft.Xml.Serialization.GeneratedAssembly.
  XmlSerializationWriterSettings.Write3_Settings(Object o)

The cause of this for me was a plugin loaded using LoadFrom context which has many disadvantages to using the Load context. Quite a bit of fun tracking that one down.

If you try to serialize an array, List<T>, or IEnumerable<T> which contains instances of subclasses of T, you need to use the XmlArrayItemAttribute to list all the subtypes being used. Otherwise you will get an unhelpful System.InvalidOperationException at runtime when you serialize.

Here is part of a full example from the documentation

public class Group
{  
   /* The XmlArrayItemAttribute allows the XmlSerializer to insert both the base 
      type (Employee) and derived type (Manager) into serialized arrays. */

   [XmlArrayItem(typeof(Manager)), XmlArrayItem(typeof(Employee))]
   public Employee[] Employees;

Private variables/properties are not serialized in the default mechanism for XML serialization, but are in binary serialization.

I can't really explain this one, but I found this won't serialise:

[XmlElement("item")]
public myClass[] item
{
    get { return this.privateList.ToArray(); }
}

but this will:

[XmlElement("item")]
public List<myClass> item
{
    get { return this.privateList; }
}

And also worth noting that if you're serialising to a memstream, you might want to seek to 0 before you use it.

If your XSD makes use of substitution groups, then chances are you can't (de)serialize it automatically. You'll need to write your own serializers to handle this scenario.

Eg.

<xs:complexType name="MessageType" abstract="true">
    <xs:attributeGroup ref="commonMessageAttributes"/>
</xs:complexType>

<xs:element name="Message" type="MessageType"/>

<xs:element name="Envelope">
    <xs:complexType mixed="false">
        <xs:complexContent mixed="false">
            <xs:element ref="Message" minOccurs="0" maxOccurs="unbounded"/>
        </xs:complexContent>
    </xs:complexType>
</xs:element>

<xs:element name="ExampleMessageA" substitutionGroup="Message">
    <xs:complexType mixed="false">
        <xs:complexContent mixed="false">
                <xs:attribute name="messageCode"/>
        </xs:complexContent>
    </xs:complexType>
</xs:element>

<xs:element name="ExampleMessageB" substitutionGroup="Message">
    <xs:complexType mixed="false">
        <xs:complexContent mixed="false">
                <xs:attribute name="messageCode"/>
        </xs:complexContent>
    </xs:complexType>
</xs:element>

In this example, an Envelope can contain Messages. However, the .NET's default serializer doesn't distinguish between Message, ExampleMessageA and ExampleMessageB. It will only serialize to and from the base Message class.

Be careful serialising types without explicit serialisation, it can result in delays while .Net builds them. I discovered this recently while serialising RSAParameters.

Private variables/properties are not serialized in XML serialization, but are in binary serialization.

I believe this also gets you if you are exposing the private members through public properties - the private members don't get serialised so the public members are all referencing null values.

Related