c# - 序列化时不忽略在 DefaultValue 中声明的数组

标签 c# json.net

我正在使用 JSON 作为配置文件,并且我想要一个数组的默认值。如果序列化的 JSON 等于 DefaultValueAttribute,我想让序列化的 JSON 忽略数组,这样如果我决定在程序的第二个版本中更改 DefaultValues,将加载新的默认值而不是原始默认值的未修改副本。

我的问题是,如果数组引用没有更改,代码可以正常工作,但程序中的其他代码正在更改数组但保留其中的值。 (该程序维护了该类的许多克隆,所以这是不可避免的)。

这里是使用 c# interactive 显示的问题:

using System.ComponentModel;
using Newtonsoft.Json;

class A
{
    [DefaultValue(new int[] { 4, 6, 12 })]
    public int[] SomeArray;
}

var serializerSettings = new JsonSerializerSettings
{
    DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
};
var a = new A();
JsonConvert.PopulateObject("{}", a, serializerSettings);

Console.WriteLine(JsonConvert.SerializeObject(a, serializerSettings));
// Prints {}

a.SomeArray = new int[] { 4, 6, 12 };
Console.WriteLine(JsonConvert.SerializeObject(a, serializerSettings));
// Prints {"SomeArray":[4,6,12]}

如您所见,第一个 SerializeObject 有效,但如果数组内容相同但不是相同的数组引用,它会将默认值写入 json,我希望避免这种情况。

有什么方法可以让 Json.net 在这种情况下忽略数组?

最佳答案

除了您已确定的问题之外,您当前的架构还有几个其他问题:

  1. 您忽略了 documented recommendations对于 DefaultValueAttribute:

    A DefaultValueAttribute will not cause a member to be automatically initialized with the attribute's value. You must set the initial value in your code.

  2. 您当前的实现导致所有具有默认值的 A 实例共享对 int[3] { 4, 6, 12 } 的单个全局实例的引用 数组。由于数组并不是真正只读的,这意味着修改 A 的一个实例将修改 A 的所有其他当前和 future 实例默认值:

    var serializerSettings = new JsonSerializerSettings
    {
        DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
    };
    var a1 = JsonConvert.DeserializeObject<A>("{}", serializerSettings);
    // The following succeeds
    Assert.IsTrue(a1.SomeArray.SequenceEqual(new int[] { 4, 6, 12 }));
    
    // Sime SomeArray is a globally shared pointer, this will modify all current and future instances of A!
    a1.SomeArray[0] = -999;
    
    var a2 = JsonConvert.DeserializeObject<A>("{}", serializerSettings);
    // The following now fails!
    Assert.IsTrue(a2.SomeArray.SequenceEqual(new int[] { 4, 6, 12 }));
    

避免这些问题的最简单方法是根本不对数组使用 DefaultValueHandling,而是使用 conditional property serialization :

class A
{
    static readonly int[] SomeArrayDefaultValue = new int[] { 4, 6, 12 };

    // Disable global settings for NullValueHandling and DefaultValueHandling
    [JsonProperty(NullValueHandling = NullValueHandling.Include, DefaultValueHandling = DefaultValueHandling.Include)]
    public int[] SomeArray = (int[])SomeArrayDefaultValue.Clone();

    public bool ShouldSerializeSomeArray()
    {
        return !(SomeArray != null && SomeArray.SequenceEqual(SomeArrayDefaultValue));
    }
}

演示 fiddle #1 here .

如果您决定对数组使用 DefaultValueHandlingDefaultValueAttribute,您将需要 custom contract resolver :

public class ArrayDefaultValueContractResolver : DefaultContractResolver
{
    class ArrayDefaultValueProvider : IValueProvider
    {
        readonly IValueProvider baseProvider;
        readonly System.Array defaultValue;

        public ArrayDefaultValueProvider(IValueProvider baseProvider, System.Array defaultValue)
        {
            this.baseProvider = baseProvider;
            this.defaultValue = defaultValue;
        }

        #region IValueProvider Members

        public object GetValue(object target)
        {
            return baseProvider.GetValue(target);
        }

        public void SetValue(object target, object value)
        {
            // Make sure the default value is cloned since arrays are not truly read only.
            if (value != null && object.ReferenceEquals(value, defaultValue))
                value = defaultValue.Clone();
            baseProvider.SetValue(target, value);
        }

        #endregion
    }

    static void AddArrayDefaultHandling<T>(JsonProperty property)
    {
        var defaultValue = (T [])property.DefaultValue;

        // If the default value has length > 0, clone it when setting it back into the object.
        if (defaultValue.Length > 0)
        {
            property.ValueProvider = new ArrayDefaultValueProvider(property.ValueProvider, defaultValue);
        }

        // Add a ShouldSerialize method that checks for memberwise array equality.
        var valueProvider = property.ValueProvider;
        var oldShouldSerialize = property.ShouldSerialize;
        Predicate<object> shouldSerialize = target =>
            {
                var array = (T[])valueProvider.GetValue(target);
                return !(array == null || array.SequenceEqual(defaultValue));
            };
        if (oldShouldSerialize == null)
            property.ShouldSerialize = shouldSerialize;
        else
            property.ShouldSerialize = (target) => shouldSerialize(target) && oldShouldSerialize(target);
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (property.PropertyType.IsArray && property.DefaultValue != null && property.DefaultValue.GetType() == property.PropertyType
            && property.PropertyType.GetArrayRank() == 1)
        {
            typeof(ArrayDefaultValueContractResolver)
                .GetMethod("AddArrayDefaultHandling", BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(property.PropertyType.GetElementType())
                .Invoke(null, BindingFlags.Static | BindingFlags.NonPublic, null, new [] { property }, null);
        }
        return property;
    }
}

要使用它,在某处缓存一个静态实例以提高性能,例如

static IContractResolver resolver = new ArrayDefaultValueContractResolver();

并将其用作 JsonSerializerSettings.ContractResolver序列化时:

var serializerSettings = new JsonSerializerSettings
{
    DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
    ContractResolver = resolver,
};
var a = new A();
JsonConvert.PopulateObject("{}", a, serializerSettings);

Console.WriteLine(JsonConvert.SerializeObject(a, serializerSettings));
Assert.IsTrue(JsonConvert.SerializeObject(a, serializerSettings) == "{}");

a.SomeArray = new int[] { 4, 6, 12 };
Console.WriteLine(JsonConvert.SerializeObject(a, serializerSettings));
Assert.IsTrue(JsonConvert.SerializeObject(a, serializerSettings) == "{}");

演示 fiddle #2 here .

注意事项:

  • 契约解析器仅针对秩为 1 的数组实现。如果需要,您可以将其扩展到多维数组。

  • 契约(Contract)解析器在将默认值数组实例设置为成员时会自动克隆它,以避免上述问题 #2。如果您不想这样,您可以删除 ArrayDefaultValueProvider

  • 尚不清楚支持数组值默认值是否是 Json.NET 的预期功能。

关于c# - 序列化时不忽略在 DefaultValue 中声明的数组,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54066654/

相关文章:

c# - 如何使用 Direct2D 在透明背景上呈现 ClearType 文本?

c# - 从 Word 复制文本并从剪贴板获取文件位置

c# - 自增外键

c# - 使用根对象属性中的大量数据解析大型 JSON 文件

c# - 单击选择性粘贴时如何在 visual studio 2012 中显示 "paste Json class"?

c# - 在 JSON 文件中追加对象的最佳方式?

c# - 未实现/支持/无效操作异步方法

c# - C# 的 OPC 示例应用程序

c# - 无法使用 Json.Net 反序列化指数数值

c# - 如何在 ReadJson 方法中从 JsonReader 获取所有字符串?