首页 > 解决方案 > JSON.NET 如何设置只获取的 IEnumerable财产?

问题描述

在以下示例中,foodeserializedFoo变量都具有完全填充的Values属性。一个私有的、无操作的构造函数被赋予该[JsonConstructor]属性以避免来自公共构造函数的构造函数值映射。

尽管如此,如果 JSON.NET 在构造期间将其支持字段分配给空列表,则似乎能够设置 get-only 属性的值。

我无法在 JSON.NET 文档中找到任何提及此行为的内容。谁能解释这种行为?

void Main()
{
    var foo = new Foo(new [] { "One", "Two", "Three" });
    var json = JsonConvert.SerializeObject(foo);
    var deserializedFoo = JsonConvert.DeserializeObject<Foo>(json);
}

class Foo
{
    private readonly List<string> _values;

    public Foo(IEnumerable<string> values)
    {
        _values = values.ToList();
    }

    [JsonConstructor]
    private Foo()
    {
        _values = new List<string>();
    }

    public IEnumerable<string> Values => _values;
}

标签: c#json.net

解决方案


我不确定这是在哪里记录的(如果有的话),但是在对象的反序列化过程中,如果已经分配了引用类型的成员,Json.NET 将重新使用现有的成员实例而不是分配新的成员实例。然后它将使用成员值的实际具体类型(而不是声明的类型)将 JSON 填充到成员中。在您的情况下,返回的对象的实际具体类型ValuesList<string>- Json.NET 能够填充的类型。

如果我更改Values为实际返回一个只读枚举,如下所示:

public IEnumerable<string> Values => _values.AsReadOnly();

然后Values不填充 - 演示小提琴 #1这里

另一方面,如果我将声明的类型更改Valuesobject

public object Values => _values;

It is populated. Demo fiddle #2 here.

And as a final demonstration, if I modify your Foo model to have some additional pre-allocated, read-only property Bar that is only declared as object like so:

class Foo
{
    private readonly List<string> _values;
    private readonly Bar _bar = new Bar();

    public Foo(IEnumerable<string> values)
    {
        _values = values.ToList();
    }

    [JsonConstructor]
    private Foo()
    {
        _values = new List<string>();
    }

    public IEnumerable<string> Values { get { return _values; } }

    public object Bar { get { return _bar; } }
}

public class Bar
{
    public string Property1 { get; set; }
}

Then the actual object referred to by the property will be successfully populated:

var json = @"{""Values"":[""One"",""Two"",""Three""],""Bar"":{""Property1"":""Test""}}";
var deserializedFoo = JsonConvert.DeserializeObject<Foo>(json);
Assert.IsTrue(deserializedFoo.Values.Count() == 3 && ((Bar)deserializedFoo.Bar).Property1 == "Test"); // No Assert

Demo fiddle #3 here.

So what are your options, if you don't want this behavior?

Firstly, you could wrap your collection in a read-only wrapper as shown above. This is not a bad idea in general, because it prevents consumers of your Foo type from simply casting Values to a List<T> and modifying it.

Secondly, you could deserialize using ObjectCreationHandling.Replace. When this setting is enabled, Json.NET will always allocate new values for pre-allocated read/write properties -- and skip over pre-allocated read-only properties. Demo fiddle #4 here. Note this prevents population of read-only member referring to any .Net reference type, not just collections.

Thirdly, you could adopt the approach from this answer to Serialize Property, but Do Not Deserialize Property in Json.Net by Pavlo Lissov and create a custom contract resolver that sets JsonProperty.ShouldDeserialize to a predicate returning false for properties that should not get deserialized.


推荐阅读