使用TypeNameHandling=auto对IEnumerable进行Json.Net序列化

11
根据Json.Net文档,所有的IEnumerable类型应该被序列化为json数组。
因此,我期望以下类:
public class MyClass
{
    public IEnumerable<string> Values { get; set; }
}

序列化为:

{
    "Values": []
}

问题是当我使用 TypeNameHandling=Auto 时,会出现以下情况:

{
    "Values": {
        "$type": "System.String[], mscorlib",
        "$values": []
    }
}

我需要TypeNameHandling=Auto应用到其他属性,但是我希望IEnumerable使用默认序列化。其他类型(例如IList)按照预期工作。

这是一个bug还是我漏掉了什么?

这里是重现问题所需的完整代码:

    [Test]
    public void Newtonsoft_serialize_list_and_enumerable()
    {
        var target = new Newtonsoft.Json.JsonSerializer
        {
            TypeNameHandling = TypeNameHandling.Auto
        };

        var myEvent = new MyClass
        {
            Values = new string[0]
        };

        var builder = new StringWriter();
        target.Serialize(builder, myEvent);
        var json = JObject.Parse(builder.ToString());

        Assert.AreEqual(JTokenType.Array, json["Values"].Type);
    }

    public class MyClass
    {
        public IEnumerable<string> Values { get; set; }
    }

我正在使用Newtonsoft 7.0.1。 更新: 这里有另一个测试,使用了更多的类型:
    [Test]
    public void Newtonsoft_serialize_list_and_enumerable()
    {
        var target = new Newtonsoft.Json.JsonSerializer
        {
            TypeNameHandling = TypeNameHandling.Auto
        };

        var myEvent = new MyClass
        {
            Values1 = new string[0],
            Values2 = new List<string>(),
            Values3 = new string[0],
            Values4 = new List<string>(),
            Values5 = new string[0]
        };

        var builder = new StringWriter();
        target.Serialize(builder, myEvent);
        var json = builder.ToString();
    }

    public class MyClass
    {
        public IEnumerable<string> Values1 { get; set; }
        public IEnumerable<string> Values2 { get; set; }
        public IList<string> Values3 { get; set; }
        public IList<string> Values4 { get; set; }
        public string[] Values5 { get; set; }
    }

以下是结果:

{
    "Values1": {
        "$type": "System.String[], mscorlib",
        "$values": []
    },
    "Values2": [],
    "Values3": {
        "$type": "System.String[], mscorlib",
        "$values": []
    },
    "Values4": [],
    "Values5": []
}

我仍然不理解为什么在不同的组合情况下会得到不同的结果。

2个回答

7
Json.Net的默认自动行为是在反序列化到IEnumerableIList字段时创建一个List实例。如果您分配了一个Array实例,那么将对象恢复到其原始实例状态的唯一方法是Json.Net添加$type元数据,这就是您看到的。 即可以有许多方法将内容反序列化到IEnumerable字段中。 通过使用List<string>实例:
var myEvent = new MyClass
{
    Values = new List<string>(),
};

使用:

public class MyClass
{
   public IEnumerable<string> Values { get; set; }
}    

序列化后的结果:

{"Values":["hello"]}

此外,如果您使用显式可构造类型或使用默认的List,则Json.Net将使用该类型并省略$type

例如:

string[] Values { get; set; } = new string[0]; // not needed
IEnumerable<string> Values { get; set; } = new string[0]; //$type needed
IEnumerable<string> Values { get; set; } = new Stack<string>(); //$type needed
IEnumerable<string> Values { get; set; } = new List<string>; // not needed
List<string> Values { get; set; } = new List<string>; // not needed

ObservableCollection<string> Values { get; set; } = 
    new ObservableCollection<string>(); // not needed

谢谢,我可能会使用这个解决方案。但我认为这是一个错误,因为行为应该对所有类型都相同,而不是根据我使用List还是array而有所不同。 - Davide Icardi
对我来说,这是不正确的。如果你把new string[0]设置为一个IList字段,你会得到相同的行为。正如我所指出的,默认反序列化行为是为EnumerableIList接口字段创建一个List实例。如果您指定了一个明确的对象类型,它将使用该类型;但是有许多方法可以实例化IEnumerable。如果你违背默认值,唯一知道你使用了什么实例的方法是添加$type元数据。 - Meirion Hughes

2
这不是bug,而是预期行为。您正在全局设置TypeNameHandling。您可以像下面这样使用属性标记来避免将它们包含在设置TypeNameHandling.Auto中,而不是全局设置类型名称处理:
[JsonProperty(TypeNameHandling = TypeNameHandling.None)]

以下是一个示例:

    public class MyClass
    {
        // Indicate the property not to apply any type handling
        [JsonProperty(TypeNameHandling=TypeNameHandling.None)]
        public IList<string> Values { get; set; }

        public string Name { get; set; }
    }

这将使您获得所需的结果,而不会受到全局声明的任何TypeNameHandling设置的影响。


通常我不喜欢在我的代码中插入序列化属性,但最终这可能是一个好的解决方案。但我不明白为什么对于其他类型,行为是不同的。例如,如果我将属性声明为IList<T>并将其设置为List<T>,我认为这是一个错误... - Davide Icardi
这是支持派生类型正确反序列化的行为。当您将其声明为全局时,它适用于所有属性。这就是为什么提供了TypeNameHandling作为属性特性的支持。也可以采用其他方式,例如将需要特殊TypeHandling的属性标记为Auto,而根本不使用全局设置。 - vendettamit
是的,我明白,但为什么IList有不同的行为?而且文档似乎证实了我的期望。 - Davide Icardi
1
你能分享链接吗?因为我在我的机器上运行代码后,使用IList和IEnumerable具有相同的结果。 - vendettamit
我已更新问题,并添加了更多的案例。我不明白为什么“array”和“List”之间的行为不同。这里是文档。最终,我希望当类型不匹配时始终具有包装对象,但实际情况并非如此。 - Davide Icardi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接