How do you deserialize XML with dynamic element names?

My XML looks like follows:

<rates>
    <rate1>1250.00</rate1>
    <rate2>1900.00</rate2>
</rates>

This is in my Serializable class:

[XmlRoot("main")]
public class Main
{
    [XmlArray("rates"), XmlAnyElement]
    public Rates Rates { get; set; }
}

public class Rates : List<Rate> { }

public class Rate
{
    [XmlAnyElement]
    public string Rate;
}

How can I deserialize the XML so that I can access it using:

var rate1 = Main.Rates[0].Rate;
var rate2 = Main.Rates[1].Rate;

You can do this by having your Rates collection implement IXmlSerializable:

public class Rates : List<Rate>, IXmlSerializable
{
    public Rates() : base() { }
    public Rates(IEnumerable<Rate> collection) : base(collection) { }

    #region IXmlSerializable Members

    XmlSchema IXmlSerializable.GetSchema()
    {
        return null;
    }

    void IXmlSerializable.ReadXml(XmlReader reader)
    {
        // for the `decodeName` delegate, you could check that the node name matches the pattern "rateN" for some integer N, if you want.
        XmlKeyValueListHelper.ReadXml(reader, this, null, s => new Rate { RateValue = s });
    }

    void IXmlSerializable.WriteXml(XmlWriter writer)
    {
        XmlKeyValueListHelper.WriteXml(writer, this, (i, rate) => "rate" + XmlConvert.ToString(i), r => r.RateValue);
    }

    #endregion
}

public class Rate
{
    public string RateValue;
}

public static class XmlKeyValueListHelper
{
    const string XsiNamespace = @"http://www.w3.org/2001/XMLSchema-instance";
    const string XsiNil = "nil";

    public static void WriteXml<T>(XmlWriter writer, IEnumerable<T> collection, Func<int, T, string> encodeName, Func<T, string> encodeValue)
    {
        int i = 0;
        foreach (var item in collection)
        {
            writer.WriteStartElement(XmlConvert.EncodeLocalName(encodeName(i, item)));
            if (item == null)
            {
                writer.WriteAttributeString(XsiNil, XsiNamespace, XmlConvert.ToString(true));
            }
            else
            {
                writer.WriteValue(encodeValue(item) ?? "");
            }
            writer.WriteEndElement();
            i++;
        }
    }

    public static void ReadXml<T>(XmlReader reader, ICollection<T> collection, Func<int, string, bool> decodeName, Func<string, T> decodeValue)
    {
        if (reader.IsEmptyElement)
        {
            reader.Read();
            return;
        }

        int i = 0;
        reader.ReadStartElement(); // Advance to the first sub element of the list element.
        while (reader.NodeType == XmlNodeType.Element)
        {
            var key = XmlConvert.DecodeName(reader.Name);
            if (decodeName == null || decodeName(i, key))
            {
                var nilValue = reader[XsiNil, XsiNamespace];
                if (!string.IsNullOrEmpty(nilValue) && XmlConvert.ToBoolean(nilValue))
                {
                    collection.Add(default(T));
                    reader.Skip();
                }
                else
                {
                    string value;
                    if (reader.IsEmptyElement)
                    {
                        value = string.Empty;
                        // Move past the end of item element
                        reader.Read();
                    }
                    else
                    {
                        // Read content and move past the end of item element
                        value = reader.ReadElementContentAsString();
                    }
                    collection.Add(decodeValue(value));
                }
            }
            else
            {
                reader.Skip();
            }
            i++;
        }
        // Move past the end of the list element
        reader.ReadEndElement();
    }
}

Example fiddle.


Dynamic element names can be simple to process using the XmlSerializer.UnknownElement event handler.

For example, we have following xml:

<main>
  <rates>
    <rate1>1250.00</rate1>
    <rate2>1900.00</rate2>
  </rates>
</main>

and classes:

[XmlRoot("main")]
public class Main
{
    public List<Rate> Rates { get; set; }
}

public class Rate
{
    public string Value { get; set; }
}

Do deserialize:

var xs = new XmlSerializer(typeof(Main));
// add event handler
xs.UnknownElement += Xs_UnknownElement;

Main main;
using (var fs = new FileStream("test.xml", FileMode.Open))
    main = (Main)xs.Deserialize(fs);

The event handler code:

private void Xs_UnknownElement(object sender, XmlElementEventArgs e)
{
    Main main = (Main)e.ObjectBeingDeserialized;

    foreach (XmlNode node in e.Element.ChildNodes)
        main.Rates.Add(new Rate { Value = node.InnerText });
}