2

I need to (de)serialize a class in C# (.NET Framework 4.5.2) to and from XML which has a dictionary property with string keys and string[] array values. I am using the SerializableDictionary<TKey, TValue> implementation mentioned in this answer on another question, so that means my property is of type SerializableDictionary<string, string[]>.

Serialization of this to an XML file looks like it works alright; however, deserializing always fails with a System.InvalidOperationException.

This occurs even when just deserializing the dictionary on its own. See the below MSTest unit test which reproduces the problem:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using My.Namespace.Collections; // Where SerializableDictionary is defined.
using System.Xml.Serialization;
using System.IO;
using System.Linq;

namespace My.Namespace.Collections.Tests
{
    /// <summary>
    /// Provides unit test methods for the <see cref="SerializableDictionary{TKey, TValue}"/> class.
    /// </summary>
    [TestClass]
    public class SerializableDictionaryTests
    {
        /// <summary>
        /// A basic test that the <see cref="SerializableDictionary{TKey, TValue}"/> class has working
        /// (de)serialization.
        /// </summary>
        [TestMethod]
        [Timeout(100)]
        public void SerializableDictionary_BasicTest()
        {
            // Arrange.
            var sourceDictionary = new SerializableDictionary<string, string[]>();
            SerializableDictionary<string, string[]> destinationDictionary;
            var serializer = new XmlSerializer(typeof(SerializableDictionary<string, string[]>));

            var list1 = new string[] { "apple", "banana", "pear" };
            var list2 = new string[] { "carrot", "broccoli", "cauliflower", "onion" };
            var list3 = new string[] { "beef", "chicken" };

            sourceDictionary.Add("fruits", list1);
            sourceDictionary.Add("vegetables", list2);
            sourceDictionary.Add("meats", list3);

            // Act.
            using (var stream = new MemoryStream())
            {
                serializer.Serialize(stream, sourceDictionary);
                stream.Position = 0;
                destinationDictionary = (SerializableDictionary<string, string[]>)serializer.Deserialize(stream);
            }

            // Assert.
            // NOTE: We don't get this far because it crashes on the last statement above in the using block.
            Assert.AreEqual(3, destinationDictionary.Keys.Count);
            Assert.IsTrue(destinationDictionary.ContainsKey("fruits"));
            Assert.IsTrue(destinationDictionary.ContainsKey("vegetables"));
            Assert.IsTrue(destinationDictionary.ContainsKey("meats"));

            Assert.AreEqual(3, destinationDictionary["fruits"].Length);
            Assert.IsTrue(destinationDictionary["fruits"].Contains("apple"));
            Assert.IsTrue(destinationDictionary["fruits"].Contains("banana"));
            Assert.IsTrue(destinationDictionary["fruits"].Contains("pear"));

            Assert.AreEqual(4, destinationDictionary["vegetables"].Length);
            Assert.IsTrue(destinationDictionary["vegetables"].Contains("carrot"));
            Assert.IsTrue(destinationDictionary["vegetables"].Contains("broccoli"));
            Assert.IsTrue(destinationDictionary["vegetables"].Contains("cauliflower"));
            Assert.IsTrue(destinationDictionary["vegetables"].Contains("onion"));

            Assert.AreEqual(2, destinationDictionary["meats"].Length);
            Assert.IsTrue(destinationDictionary["meats"].Contains("beef"));
            Assert.IsTrue(destinationDictionary["meats"].Contains("chicken"));
        }
    }
}

The exception is at the serializer.Deserialize call, and reads:

Test method My.Namespace.Collections.Tests.SerializableDictionaryTests.SerializableDictionary_BasicTest threw exception: 
System.InvalidOperationException: There is an error in XML document (8, 8). ---> System.InvalidOperationException: There is an error in XML document (8, 8). ---> System.InvalidOperationException: <ArrayOfString xmlns=''> was not expected.

Why am I getting this exception and how can I avoid it? I would like to avoid having to resort to custom XML serialization for just this one property, if possible.

Edit #1:

I made a small console program which runs the above construction and serialization code, then writes the resulting XML to a file using the FileStream class. Here is its contents:

<?xml version="1.0"?>
<dictionary>
  <item>
    <key>
      <string>fruits</string>
    </key>
    <value>
      <ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <string>apple</string>
        <string>banana</string>
        <string>pear</string>
      </ArrayOfString>
    </value>
  </item>
  <item>
    <key>
      <string>vegetables</string>
    </key>
    <value>
      <ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <string>carrot</string>
        <string>broccoli</string>
        <string>cauliflower</string>
        <string>onion</string>
      </ArrayOfString>
    </value>
  </item>
  <item>
    <key>
      <string>meats</string>
    </key>
    <value>
      <ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <string>beef</string>
        <string>chicken</string>
      </ArrayOfString>
    </value>
  </item>
</dictionary>

Edit #2:

After some additional testing changing the TValue type of the SerializableDictionary<TKey, TValue> declaration in my tests to different things and check whether it works at all, I can confirm that it works okay if TValue is string, but the same error results if it is set to, say, a custom serializable class of mine, even if that class is decorated with SerializableAttribute or implements the IXmlSerializable interface. So, the problem is actually bigger than just with string arrays.

Knowledge Cube
  • 990
  • 12
  • 35
  • Did you consider this answer: https://stackoverflow.com/a/9302922/982149 ? – Fildor Mar 19 '18 at 17:29
  • Can you show the resulting xml? – Fildor Mar 19 '18 at 17:37
  • @Fildor I'm considering that as a last resort. This is actually for just a small part of a much larger composite object being serialized, and going back through to decorate all of my child classes and properties with data contract attributes could take a while. – Knowledge Cube Mar 20 '18 at 15:39
  • When setting up the **de**serializer , did you include configuring it to know these xmlns? – Fildor Mar 20 '18 at 15:41
  • @Fildor I did not add the XML namespaces listed in my sample XML above; the serializer must have added them on its own. I cannot find any information on how the deserializer can be made aware of these, so if you can share an example I can test with then I would appreciate it. – Knowledge Cube Mar 20 '18 at 16:16

2 Answers2

2

After struggling for a couple days with this, I came to the conclusion that the Serializable<TKey, TValue> implementation I was using can't be massaged to work with arrays and other objects.

I resorted to implementing the IXmlSerializable interface on my class, with the dictionary values serialized as a list in the WriteXml method. In ReadXml, I simply deserialize the stored list, and put its values back into the dictionary while checking that there are no duplicate keys stored as an additional property on the list items. It's clunky, but it works.

Knowledge Cube
  • 990
  • 12
  • 35
2

I am glad you have found a solution and managed to get it working. Not sure if you are still open to solutions. Nevertheless, here's what you could have done to live with Dictionary as is.

Although .NET's XmlSerializer does not support dictionary, there are libs that can do it for you. One such lib is SharpSerializer (Source, NuGet) which serializes any type into binary or XML.

Usage is as easy as:

var serializer = new SharpSerializer(); 
serializer.Serialize(dict, "test.xml");

Input

var dict = new Dictionary<string, string[]> 
{
    { "nikhil", new string[] { "nikhil.0@so.com", "nikhil.1@so.com" } } 
};

Output

<Dictionary name="Root" type="System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
  <Items>
    <Item>
      <Simple value="nikhil" />
      <SingleArray>
        <Items>
          <Simple value="nikhil.0@so.com" />
          <Simple value="nikhil.1@so.com" />
        </Items>
      </SingleArray>
    </Item>
  </Items>
</Dictionary>

Fiddle

Nikhil Vartak
  • 5,002
  • 3
  • 26
  • 32
  • I would like to avoid adding dependencies on third-party code, but I will keep this in mind for future projects. Thanks! – Knowledge Cube Mar 22 '18 at 15:37
  • Just a thought - We live in an era of open source with awesome developers who have done all the hard work for us already, and more efficiently than us. It really surprises me when I hear hesitation about dependencies on 3rd party libs. – Nikhil Vartak Mar 22 '18 at 22:53