Blaise
Blaise

Reputation: 22212

How to deserialize xml into Dictionary when key is the Xml node name?

Here is the xml

<?xml version="1.0"?>
<TransactionLog>
  <RuleViolations>
    <error>
      <message>error1</message>
      <keys>
        <key1>val1</key1>
        <key2>val2</key2>
        <key3>val3</key3>
        <key4>val4</key4>
      </keys>
    </error>
    <error>
      <message>error1</message>
      <keys>
        <key1>val5</key1>
        <key2>val6</key2>
      </keys>
    </error>
    <error>
      <message>error3</message>
      <keys>
        <key2>val7</key2>
        <key3>val8</key3>
        <key4>val9</key4>
      </keys>
    </error>
  </RuleViolations>
</TransactionLog>

What I have now:

[XmlRoot("TransactionLog")]
public class TransactionLogModel
{
    [XmlArray("RuleViolations")]
    [XmlArrayItem("error")]
    public List<KeyValuePair<string,string>> RuleViolations { get; set; }
}

But how can we serialize the <keys> section?


The closest SO post I can find is here: Deserialize XML into Dictionary

But I am not using XDocument.

var x = new XmlSerializer(typeof(TransactionLogModel));
var model = (TransactionLogModel)x.Deserialize(new StringReader(log));    

How can we deserialize this xml in XmlSerializer?

Upvotes: 1

Views: 1063

Answers (1)

dbc
dbc

Reputation: 116826

Firstly, your data model doesn't match your XML -- there are several intermediate classes missing between TransactionLog and keys. Instead, it should look something like:

[XmlRoot("TransactionLog")]
public class TransactionLogModel
{
    [XmlElement("RuleViolations")]
    public List<RuleViolation> RuleViolations { get; set; }
}

public class RuleViolation
{
    public RuleViolation() { this.Errors = new List<Error>(); }

    [XmlElement("error")]
    public List<Error> Errors { get; set; }
}

public class Error
{
    [XmlElement("message")]
    public string Message { get; set; }

    // To be done.
    public List<KeyValuePair<string, string>> Keys { get; set; }
}

Next, to serialize the List<KeyValuePair<string, string>> Keys using the key names as element names, the standard solution is to implement IXmlSerializable on an appropriate type. It's a bit of a nuisance but not awful since your pair values are primitive types (strings) rather that complex types requiring nested serializations.

For instance, you could use the XmlKeyTextValueListWrapper from Serialize Dictionary member to XML elements and data:

public class XmlKeyTextValueListWrapper<TValue> : CollectionWrapper<KeyValuePair<string, TValue>>, IXmlSerializable
{
    public XmlKeyTextValueListWrapper() : base(new List<KeyValuePair<string, TValue>>()) { } // For deserialization.

    public XmlKeyTextValueListWrapper(ICollection<KeyValuePair<string, TValue>> baseCollection) : base(baseCollection) { }

    public XmlKeyTextValueListWrapper(Func<ICollection<KeyValuePair<string, TValue>>> getCollection) : base(getCollection) {}

    #region IXmlSerializable Members

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        var converter = TypeDescriptor.GetConverter(typeof(TValue));
        XmlKeyValueListHelper.ReadXml(reader, this, converter);
    }

    public void WriteXml(XmlWriter writer)
    {
        var converter = TypeDescriptor.GetConverter(typeof(TValue));
        XmlKeyValueListHelper.WriteXml(writer, this, converter);
    }

    #endregion
}

public static class XmlKeyValueListHelper
{
    public static void WriteXml<T>(XmlWriter writer, ICollection<KeyValuePair<string, T>> collection, TypeConverter typeConverter)
    {
        foreach (var pair in collection)
        {
            writer.WriteStartElement(XmlConvert.EncodeName(pair.Key));
            writer.WriteValue(typeConverter.ConvertToInvariantString(pair.Value));
            writer.WriteEndElement();
        }
    }

    public static void ReadXml<T>(XmlReader reader, ICollection<KeyValuePair<string, T>> collection, TypeConverter typeConverter)
    {
        if (reader.IsEmptyElement)
        {
            reader.Read();
            return;
        }

        reader.ReadStartElement(); // Advance to the first sub element of the list element.
        while (reader.NodeType == XmlNodeType.Element)
        {
            var key = XmlConvert.DecodeName(reader.Name);
            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(new KeyValuePair<string,T>(key, (T)typeConverter.ConvertFromInvariantString(value)));
        }
        // Move past the end of the list element
        reader.ReadEndElement();
    }

    public static void CopyTo<TValue>(this XmlKeyTextValueListWrapper<TValue> collection, ICollection<KeyValuePair<string, TValue>> dictionary)
    {
        if (dictionary == null)
            throw new ArgumentNullException("dictionary");
        if (collection == null)
            dictionary.Clear();
        else
        {
            if (collection.IsWrapperFor(dictionary)) // For efficiency
                return;
            var pairs = collection.ToList();
            dictionary.Clear();
            foreach (var item in pairs)
                dictionary.Add(item);
        }
    }
}

public class CollectionWrapper<T> : ICollection<T>
{
    readonly Func<ICollection<T>> getCollection;

    public CollectionWrapper(ICollection<T> baseCollection)
    {
        if (baseCollection == null)
            throw new ArgumentNullException();
        this.getCollection = () => baseCollection;
    }

    public CollectionWrapper(Func<ICollection<T>> getCollection)
    {
        if (getCollection == null)
            throw new ArgumentNullException();
        this.getCollection = getCollection;
    }

    public bool IsWrapperFor(ICollection<T> other)
    {
        if (other == Collection)
            return true;
        var otherWrapper = other as CollectionWrapper<T>;
        return otherWrapper != null && otherWrapper.IsWrapperFor(Collection);
    }

    ICollection<T> Collection { get { return getCollection(); } }

    #region ICollection<T> Members

    public void Add(T item)
    {
        Collection.Add(item);
    }

    public void Clear()
    {
        Collection.Clear();
    }

    public bool Contains(T item)
    {
        return Collection.Contains(item);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        Collection.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return Collection.Count; }
    }

    public bool IsReadOnly
    {
        get { return Collection.IsReadOnly; }
    }

    public bool Remove(T item)
    {
        return Collection.Remove(item);
    }

    #endregion

    #region IEnumerable<T> Members

    public IEnumerator<T> GetEnumerator()
    {
        return Collection.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion
}

Then use it like:

public class Error
{
    [XmlElement("message")]
    public string Message { get; set; }

    List<KeyValuePair<string, string>> keys;

    [XmlIgnore]
    public List<KeyValuePair<string, string>> Keys
    {
        get
        {
            // Ensure keys is never null.
            return (keys = keys ?? new List<KeyValuePair<string, string>>());
        }
        set
        {
            keys = value;
        }
    }

    [XmlElement("keys")]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public XmlKeyTextValueListWrapper<string> XmlKeys
    {
        get
        {
            return new XmlKeyTextValueListWrapper<string>(() => this.Keys);
        }
        set
        {
            value.CopyTo(Keys);
        }
    }
}

Incidentally, the same solution will work with a public Dictionary<string, string> Keys property, just be sure that the dictionary is pre-allocated:

public class Error
{
    [XmlElement("message")]
    public string Message { get; set; }

    Dictionary<string, string> keys;

    [XmlIgnore]
    public Dictionary<string, string> Keys
    {
        get
        {
            // Ensure keys is never null.
            return (keys = keys ?? new Dictionary<string, string>());
        }
        set
        {
            keys = value;
        }
    }

    [XmlElement("keys")]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public XmlKeyTextValueListWrapper<string> XmlKeys
    {
        get
        {
            return new XmlKeyTextValueListWrapper<string>(() => this.Keys);
        }
        set
        {
            value.CopyTo(Keys);
        }
    }
}

Upvotes: 1

Related Questions