应用 [DataContract] 反序列化 IEnumerable<T> 不起作用

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

对 Json.net 比较陌生,并尝试了以下简单示例序列化然后反序列化一个对象,得到以下错误:

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();
        }
    }
}

然后出现以下错误:

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.

我做错了什么?

更新 2

为了向后兼容,这将在 11.0.2 中恢复。参考原答案解决。

更新

报告为 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 可能是 Json.NET 版本 11。

原回答

我注意到您已将 Activities class 标记为 [DataContract][DataMember]:

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

应用 [DataContact] 将导致 DataContractJsonSerializerIEnumerable<T> 序列化为具有属性的 JSON 对象,而不是 JSON 数组。由于 Json.NET supports data contract attributes 应用于非可枚举对象时,您可能会认为它也会在可枚举对象和集合上尊重它们。

但是,这似乎没有实现。如果我用 DataContractJsonSerializer 序列化你的 class,我会看到

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

但是如果我使用 Json.NET 进行序列化,我会看到 [DataContract] 被忽略了:

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

然后它在反序列化期间抛出异常,因为它不知道如何将成员添加到您的 IEnumerable<Activity> class。 (如果您的 class 实现了 ICollection<Activity>had a constructor with an IEnumerable<Activity> argument,它就可以添加成员。)

那么,这行得通吗?文档页面 Serialization Attributes 指出:

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

这意味着 Json.NET 应该按您期望的方式工作。如果你愿意,你可以 report an issue 关于它——至少应该澄清文档。

作为解决方法,如果你想强制Json.NET将一个集合序列化为一个对象,你需要使用[JsonObject]代替:

[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.
}

如果您有许多可枚举的 class 应用了 [DataContract],或者无法向您的模型添加对 Json.NET 的依赖,您可以创建一个 custom ContractResolver 来检查因为 [DataContract] 存在于可枚举的 classes 上并将它们序列化为对象:

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);
    }
}

然后使用以下设置:

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