使用自定义 ContractResolver,如何在将 null JSON 属性 反序列化为值类型成员时设置默认值而不是 null?

Using a custom ContractResolver, how to set a default value instead of null when deserializing a null JSON property to a value-type member?

这是我到目前为止所得到的。感谢:

public class JsonSerializeTest
{
    [Fact]
    public void deserialize_test()
    {
        var settings = new JsonSerializerSettings { ContractResolver = new CustomContractResolver() };

        var jsonString = "{\"PropertyA\":\"Test\",\"PropertyB\":null}";
        var jsonObject = JsonConvert.DeserializeObject<NoConfigModel>(jsonString, settings);
        Assert.NotNull(jsonObject);
        
    }
}

public class NoConfigModel
{
    public string PropertyA { get; set; }
    public int PropertyB { get; set; }
    public bool PropertyC { get; set; }

}

class CustomContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);
        property.ShouldDeserialize = instance =>
        {
            try
            {
                PropertyInfo prop = (PropertyInfo)member;
                if (prop.CanRead)
                {
                    var value = prop.GetValue(instance, null);// getting default value(0) here instead of null for PropertyB
                    return value != null;
                }
            }
            catch
            {
            }
            return false;
        };
        return property;
    }
}

我的问题:

需要将默认值设置为 Not Nullable 字段,而不是 Exception 或整个对象为 null。缺少值不是问题(由 DefaultContractResolver 给出默认值),但是当在 json 中将不可空值显式设置为 null 时,则会出现异常。

我上面的代码很接近但不够接近。我想我需要找到一种方法来知道 json 中的值实际上为空,并为这些情况设置 ShouldDeserialize =false

你想要的是,在反序列化过程中,当遇到非空成员的null值时,设置一个默认(非空)值 回到包含对象中。这可以通过覆盖 DefaultContractResolver.CreateProperty 来完成,如下所示:

class CustomContractResolver : DefaultContractResolver
{
    class NullToDefaultValueProvider : ValueProviderDecorator
    {
        readonly object defaultValue;

        public NullToDefaultValueProvider(IValueProvider baseProvider, object defaultValue) : base(baseProvider)
        {
            this.defaultValue = defaultValue;
        }

        public override void SetValue(object target, object value)
        {
            base.SetValue(target, value ?? defaultValue);
        }
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);
        if (property != null && property.PropertyType.IsValueType && Nullable.GetUnderlyingType(property.PropertyType) == null && property.Writable)
        {
            var defaultValue = property.DefaultValue ?? Activator.CreateInstance(property.PropertyType);

            // When a null value is encountered in the JSON we want to set a default value in the class.
            property.PropertyType = typeof(Nullable<>).MakeGenericType(new[] { property.PropertyType });
            property.ValueProvider = new NullToDefaultValueProvider(property.ValueProvider, defaultValue);

            // Remember that the underlying property is actually not nullable so GetValue() will never return null.
            // Thus the below just overrides JsonSerializerSettings.NullValueHandling to force the value to be set
            // (to the default) even when null is encountered.
            property.NullValueHandling = NullValueHandling.Include;
        }
        return property;
    }

    // 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."
    // See also 
    static CustomContractResolver instance;

    // Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
    static CustomContractResolver() { instance = new CustomContractResolver(); }

    public static CustomContractResolver Instance { get { return instance; } }

}

public abstract class ValueProviderDecorator : IValueProvider
{
    readonly IValueProvider baseProvider;

    public ValueProviderDecorator(IValueProvider baseProvider)
    {
        if (baseProvider == null)
            throw new ArgumentNullException();
        this.baseProvider = baseProvider;
    }

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

    public virtual void SetValue(object target, object value) { baseProvider.SetValue(target, value); }
}

备注:

  • 合同解析器的工作方式是将返回非空值的属性类型更改为相应的 Nullable<T> 类型,然后创建一个 ValueProvider decorator 来映射传入的 null 值更改为以前的默认值(不能为空,因为基础类型不可为空)。

  • 没有必要重写JsonProperty.ShouldDeserialize。此谓词允许根据 目标对象 的状态动态忽略 JSON 属性 值。它甚至没有传递反序列化的 JSON 值。

  • 如果您的类型使用 parameterized constructor you may also need to override DefaultContractResolver.CreatePropertyFromConstructorParameter.

  • 您可能需要 以获得最佳性能。

工作示例 .Net fiddle here.