Search code examples
c#jsonjson.netdatacontract

Deserializing an IEnumerable<T> with [DataContract] applied does not work


Rather new to Json.net and tried the following simple example serializing and then deserialing an object getting the error below:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Collections;

namespace Timehunter.Base.ServicesTests
{
    /// <summary>
    /// Summary description for JsonError
    /// </summary>
    [TestClass]
    public class JsonError
    {
  [TestMethod]
        public void TestMethod1()
        {
            JsonSerializerSettings serializerSettings = new JsonSerializerSettings()
            {
                DateFormatHandling = DateFormatHandling.IsoDateFormat,
                DateParseHandling = Newtonsoft.Json.DateParseHandling.DateTimeOffset
            };

            Act.Activities acts = new Act.Activities();
            acts.Add(new Act.Activity() { Id = 1, Name = "test1" });
            acts.Add(new Act.Activity() { Id = 2, Name = "test2" });
            string json = Newtonsoft.Json.JsonConvert.SerializeObject(acts, serializerSettings);

            Timehunter.Base.Act.Activities target = Newtonsoft.Json.JsonConvert.DeserializeObject<Timehunter.Base.Act.Activities>(json, serializerSettings);
            Assert.AreEqual("test1", target.List[0].Name, "Name of first activity");
        }
    }
}
namespace Timehunter.Base
{
    [DataContract]
    public class Activity
    {
        private int _id;
        private string _name;

        [DataMember]
        public int Id
        {
            get { return this._id; }
            set { this._id = value; }
        }
        [DataMember]
        public string Name
        {
            get { return this._name; }
            set { this._name = value; }
        }

        public Activity()
        {
            this._id = new int();
            this._name = string.Empty;
        }
    }
    [DataContract]
    public class Activities : IEnumerable<Activity>
    {
        private List<Activity> _list;
        [DataMember]
        public List<Activity> List
        {
            get { return this._list; }
            set { this._list = value; }
        }
        public Activities()
        {
            this._list = new List<Activity>();
        }

        public void Add(Activity item)
        { this._list.Add(item); }

        public bool Remove(Activity item)
        { return this._list.Remove(item); }

        public int Count()
        { return this._list.Count; }

        public IEnumerator<Activity> GetEnumerator()
        {
            return this._list.GetEnumerator();
        }

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

And then I get the following error:

Test Name:  TestMethod1
Test FullName:  Timehunter.Base.ServicesTests.JsonError.TestMethod1
Test Source:    C:\Users\hawi.HAWCONS\Documents\Visual Studio 2015\Projects\Timehunter.Data\Timehunter.Base.ServicesTests\JsonError.cs : line 67
Test Outcome:   Failed
Test Duration:  0:00:00,2038359

Result StackTrace:  
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewList(JsonReader reader, JsonArrayContract contract, Boolean& createdFromNonDefaultCreator)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateList(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, Object existingValue, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at Timehunter.Base.ServicesTests.JsonError.TestMethod1() in C:\Users\hawi.HAWCONS\Documents\Visual Studio 2015\Projects\Timehunter.Data\Timehunter.Base.ServicesTests\JsonError.cs:line 79
Result Message: 
Test method Timehunter.Base.ServicesTests.JsonError.TestMethod1 threw exception: 
Newtonsoft.Json.JsonSerializationException: Cannot create and populate list type Timehunter.Base.Act.Activities. Path '', line 1, position 1.

What am I doing wrong?


Solution

  • Update 2

    This is getting reverted back in 11.0.2 for backwards compatibility. Refer to the original answer for a solution.

    Update

    Reported as Issue #1598: DataContractAttribute does not cause JSon object serialization for IEnumerable and fixed in commit e9e2d00. It should be in the next release after 10.0.3 which will probably be Json.NET version 11.

    Original answer

    I notice you have marked your Activities class with [DataContract] and [DataMember]:

    [DataContract]
    public class Activities : IEnumerable<Activity>
    {
        private List<Activity> _list;
        [DataMember]
        public List<Activity> List
        {
            get { return this._list; }
            set { this._list = value; }
        }
        // ...
    }
    

    Applying [DataContact] will cause DataContractJsonSerializer to serialize an IEnumerable<T> as a JSON object with properties, rather than as a JSON array. Since Json.NET supports data contract attributes when applied to non-enumerables, you might be thinking that it will respect them on enumerables and collections as well.

    However, it appears this is not implemented. If I serialize your class with DataContractJsonSerializer, I see

    {"List":[{"Id":1,"Name":"test1"},{"Id":2,"Name":"test2"}]}
    

    But if I serialize with Json.NET, I see that the [DataContract] was ignored:

    [{"Id":1,"Name":"test1"},{"Id":2,"Name":"test2"}]
    

    Then later it throws an exception during deserialization because it doesn't know how to add members to your IEnumerable<Activity> class. (It would have been able to add members if your class implemented ICollection<Activity>, or had a constructor with an IEnumerable<Activity> argument.)

    So, should this work? The documentation page Serialization Attributes states:

    The DataContractAttribute can be used as substitute for JsonObjectAttribute. The DataContractAttribute will default member serialization to opt-in.

    Which implies that Json.NET ought to work the way you expect. You could report an issue about it if you want -- at least the documentation should be clarified.

    As a workaround, if you want to force Json.NET to serialize a collection as an object, you need to use [JsonObject] instead:

    [DataContract]
    [JsonObject(MemberSerialization = MemberSerialization.OptIn)]
    public class Activities : IEnumerable<Activity>
    {
        private List<Activity> _list;
    
        [DataMember]
        [JsonProperty]
        public List<Activity> List
        {
            get { return this._list; }
            set { this._list = value; }
        }
    
        // Remainder unchanged.
    }
    

    If you have many enumerable classes with [DataContract] applied, or cannot add a dependency on Json.NET to your models, you can create a custom ContractResolver that checks for the presence of [DataContract] on enumerable classes and serializes them as objects:

    public class DataContractForCollectionsResolver : DefaultContractResolver
    {
        // As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
        // http://www.newtonsoft.com/json/help/html/ContractResolver.htm
        // http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
        // "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
        static DataContractForCollectionsResolver instance;
    
        static DataContractForCollectionsResolver() { instance = new DataContractForCollectionsResolver(); }
    
        public static DataContractForCollectionsResolver Instance { get { return instance; } }
    
        protected DataContractForCollectionsResolver() : base() { }
    
        protected override JsonContract CreateContract(Type objectType)
        {
            var t = (Nullable.GetUnderlyingType(objectType) ?? objectType);
            if (!t.IsPrimitive 
                && t != typeof(string)
                && !t.IsArray
                && typeof(IEnumerable).IsAssignableFrom(t) 
                && !t.GetCustomAttributes(typeof(JsonContainerAttribute),true).Any()) 
            { 
                if (t.GetCustomAttributes(typeof(DataContractAttribute),true).Any()) 
                    return base.CreateObjectContract(objectType);
            }
            return base.CreateContract(objectType);
        }
    }
    

    Then use the following settings:

    var serializerSettings = new JsonSerializerSettings()
    {
        DateFormatHandling = DateFormatHandling.IsoDateFormat,
        DateParseHandling = Newtonsoft.Json.DateParseHandling.DateTimeOffset,
        ContractResolver = DataContractForCollectionsResolver.Instance
    };