1

XmlSerializer is calling IList<T>.Add() on my class and I don't understand why.

I have a custom class (one of several classes in a hierarchy) containing data that I am converting to and from XML using XmlSerializer. In a previous version of my code, these classes did not implement any interfaces, and XML serialization and deserialization both seemed to work as expected.

I'm now working on some other code that uses the data contained in this class, and I thought it would be helpful if I could access the data through the IList<T> interface, so I modified my class to implement that interface. (The "T" in this case is another one of my custom classes.) This didn't involve adding any new fields to the class; I implemented all the required methods and properties in terms of data that was already being stored.

I was hoping that this would not affect the serialization in any way. However, when deserializing XML data into my class, something is now calling the new Add() method I implemented as part of the IList<T> interface (which is a problem because this particular list IsReadOnly and so Add() throws a NotSupportedException).

This happens even when the XML node for my class is simply <myClass/> with no XML attributes or children whatsoever; the XmlSerializer is apparently still creating a new myOtherClass (which is not named anywhere in the XML document) and trying to Add() it to the myClass.

I'm having trouble searching for information in this, because most questions involving XmlSerializer and IList<T> seem to involve people trying to serialize/deserialize a variable of type IList<T>. That is NOT my situation; I have no variables of type IList<T> anywhere in the code. My class serializes and deserializes just fine if I do NOT implement the IList<T> interface.

Can anyone explain to me why XmlSerializer is calling IList<T>.Add() on my class, and/or how to make it stop?

Suggestions should ideally be compatible with this code eventually running inside Unity3d (.NET 2.0).

  • 2
    Can you not just implement `IReadOnlyList` instead of `IList`? It's a bit of a code smell to implement an interface and throw exceptions in some of the methods. – Rob Jul 22 '15 at 02:14
  • I was not familiar with `IReadOnlyList`, so thanks for pointing that out. However, this code should ideally work inside Unity3d, which I believe limits me to .NET 2.0. – Jeremy Lennert Jul 22 '15 at 17:41

2 Answers2

4

XmlSerializer requires all collections to have an Add() method, as is spelled out in the documentation:

The XmlSerializer gives special treatment to classes that implement IEnumerable or ICollection. A class that implements IEnumerable must implement a public Add method that takes a single parameter. The Add method's parameter must be of the same type as is returned from the Current property on the value returned from GetEnumerator, or one of that type's bases. A class that implements ICollection (such as CollectionBase) in addition to IEnumerable must have a public Item indexed property (indexer in C#) that takes an integer, and it must have a public Count property of type integer. The parameter to the Add method must be the same type as is returned from the Item property, or one of that type's bases. For classes that implement ICollection, values to be serialized are retrieved from the indexed Item property, not by calling GetEnumerator.

Further, if a collection has its own settable properties, these will not be serialized. This is also spelled out in the docs:

The following items can be serialized using the XmLSerializer class:

  • Classes that implement ICollection or IEnumerable: Only collections are serialized, not public properties.

To see how this plays out in practice, consider the following class:

namespace V1
{
    // https://stackoverflow.com/questions/31552724/how-why-does-xmlserializer-treat-a-class-differently-when-it-implements-ilistt
    public class Vector2
    {
        public double X { get; set; }

        public double Y { get; set; }

        public Vector2() { }

        public Vector2(double x, double y)
            : this()
        {
            this.X = x;
            this.Y = y;
        }

        public double this[int coord]
        {
            get
            {
                switch (coord)
                {
                    case 0:
                        return X;
                    case 1:
                        return Y;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
            set
            {
                switch (coord)
                {
                    case 0:
                        X = value;
                        break;
                    case 1:
                        Y = value;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }
    }
}

If I serialize this to XML, I get:

<Vector2>
    <X>1</X>
    <Y>2</Y>
</Vector2>

Now say I want a new version of this that implements IList<double>. I add the interface and implement it, throwing exceptions for all methods that resize the list:

namespace V2
{
    // https://stackoverflow.com/questions/31552724/how-why-does-xmlserializer-treat-a-class-differently-when-it-implements-ilistt
    public class Vector2 : V1.Vector2, IList<double>
    {
        public Vector2() : base() { }

        public Vector2(double x, double y) : base(x, y) { }

        #region IList<double> Members

        public int IndexOf(double item)
        {
            for (var i = 0; i < Count; i++)
                if (this[i] == item)
                    return i;
            return -1;
        }

        public void Insert(int index, double item)
        {
            throw new NotImplementedException();
        }

        public void RemoveAt(int index)
        {
            throw new NotImplementedException();
        }

        #endregion

        #region ICollection<double> Members

        public void Add(double item)
        {
            throw new NotImplementedException();
        }

        public void Clear()
        {
            throw new NotImplementedException();
        }

        public bool Contains(double item)
        {
            return IndexOf(item) >= 0;
        }

        public void CopyTo(double[] array, int arrayIndex)
        {
            foreach (var item in this)
                array[arrayIndex++] = item;
        }

        public int Count
        {
            get { return 2; }
        }

        public bool IsReadOnly
        {
            get { return true; }
        }

        public bool Remove(double item)
        {
            throw new NotImplementedException();
        }

        #endregion

        #region IEnumerable<double> Members

        public IEnumerator<double> GetEnumerator()
        {
            yield return X;
            yield return Y;
        }

        #endregion

        #region IEnumerable Members

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

        #endregion
    }
}

Now if I serialize the XML, I get:

<ArrayOfDouble>
    <double>1</double>
    <double>2</double>
</ArrayOfDouble>

As you can see, it now serializes as a collection of doubles, with the settable properties X and Y omitted. Then, when deserialized, the Add() method will get called instead of the set methods for X and Y, and throw an exception.

If I try to implement IReadOnlyList<double> instead of IList<double>, the XmlSerializer constructor now throws an exception because of the missing Add() method.

Example fiddle.

There is no way for force XmlSerializer to treat a collection as a straightforward object, other than to implement IXmlSerializable and do it manually, which is quite burdensome. (There is a workaround with DataContractSerializer, namely to apply [DataContract] instead of [CollectionDataContract] -- however DataContractSerializer was not introduced until .Net 3.5., so that's out.)

Instead of implementing IList<T>, you might want to simply introduce an extension method to iterate through the values in your class, like so:

    public static class Vector2Extensions
    {
        public static IEnumerable<double> Values(this Vector2 vec)
        {
            if (vec == null)
                throw new ArgumentNullException();
            yield return vec.X;
            yield return vec.Y;
        }
    }
dbc
  • 104,963
  • 20
  • 228
  • 340
0

Without a good, minimal, complete code example that reliably reproduces the problem, it will be impossible to provide any specific answer.

In lieu of that, here are some non-specific notes that may help you:

  1. .NET serialization treats collection types differently from other types. By default, a type that implements any IEnumerable interface (e.g. IList<T>) is considered a collection. Such types are serialized by enumerating the collection and storing the individual elements. On deserialization, .NET assumes it can use an Add() method to populate the deserialized object. Woe unto any type that throws an exception from the Add() method or, worse, doesn't implement one at all.
  2. In some cases, it may be appropriate to mark your type with the [DataContract] attribute. This overrides the default behavior, allowing your type to be treated as a non-collection type.
  3. In other cases, you really should not have implemented IList<T> in the first place, but instead should have exposed the enumeration of elements differently, e.g. as a property that returns the enumeration. Lacking a good code example (well, any code example) it's not possible to say whether this is true in your scenario or not, but I'd say there's at least a 50/50 chance it is.
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • You are saying that .NET serialization assumes the existence of an `Add()` method on any object that implements `IEnumerable`, even though `Add()` is _not_ part of the `IEnumerable` interface? – Jeremy Lennert Jul 22 '15 at 17:40
  • _"Assumes"_ is the wrong word. _"Requires"_ is more correct, in the context of serialization. Obviously, the `IEnumerable` interface itself does not require an `Add()` implementation. But serialization assumes one will deserialize at some point; to deserialize a collection requires some mechanism for populating that collection. Microsoft chose to make the requirement simple and independent of the actual interface implemented: the type must have an `Add()` method. See [Collection Types in Data Contracts](https://msdn.microsoft.com/en-us/library/aa347850(v=vs.110).aspx) for more details. – Peter Duniho Jul 22 '15 at 19:05
  • I think you're confusing `XmlSerializer` with `DataContractSerializer`. Adding `[DataContract]` will have no effect on `XmlSerializer`. – dbc Jul 22 '15 at 21:12
  • @dbc: you are correct, hence "in some cases" (i.e. when using `DataContractSerializer` and scenarios where it's implicitly used). My answer is intended to _broadly_ address the general question of serialization of collections. `XmlSerializer` has a lot of the same behaviors as `DataContractSerializer`, and so I've included points relevant to both. – Peter Duniho Jul 22 '15 at 21:21