如何强制为 null 值调用 JsonConverter.WriteJson()

Dav*_* S. 5 c# json.net

我想用一些元数据将一些属性包装在 JSON 对象中,无论它是否为空。但是,JsonConverter.WriteJson如果属性为 ,则不会调用我的自定义覆盖null

当属性不为空时我得到什么:

{"Prop":{"Version":1, "Object":{"Content":"abc"}}}
Run Code Online (Sandbox Code Playgroud)

当它为空时我得到什么:

{"Prop":null}
Run Code Online (Sandbox Code Playgroud)

当它为空时我想要什么:

{"Prop":{"Version":1, "Object":null}}
Run Code Online (Sandbox Code Playgroud)

由于WriteJson从未被要求提供空值,我没有机会控制这种行为。有什么办法可以强制这样做吗?

请注意,我想知道这是否可以与转换器或合约解析器等一起使用,我不能/不想更改MyContentWrap类(见下文)。

class VersioningJsonConverter : JsonConverter
{
    //Does not get called if value is null !!
    public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
    {
        writer.WriteStartObject();
        writer.WritePropertyName("v");
        writer.WriteValue(1);
        writer.WritePropertyName("o");
        if(value == null)
        {
            //never happens
            writer.WriteNull();
        }
        else
        {
            writer.WriteStartObject();
            writer.WritePropertyName("Content");
            writer.WriteValue((value as MyContent).Content);                
            writer.WriteEndObject();
        }
        writer.WriteEndObject();
    }
    public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer)
        => throw new NotImplementedException();
    public override Boolean CanConvert(Type objectType) => objectType == typeof(MyContent);
    public override Boolean CanRead => false;
}

public class MyContent
{
    public String Content {get;set;}
}

public class Wrap
{
    public MyContent Prop {get;set;}
}
Run Code Online (Sandbox Code Playgroud)

dbc*_*dbc 10

当前无法调用 Json.NET 来JsonConverter.WriteJson()获取null值。可以看出,JsonSerializerInternalWriter.SerializeValue(...)立即写入 null 并返回 null 传入值:

private void SerializeValue(JsonWriter writer, object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)
{
    if (value == null)
    {
        writer.WriteNull();
        return;
    }
    // Remainder omitted
Run Code Online (Sandbox Code Playgroud)

因此,如果您需要将null成员转换为非空 JSON 值但无法修改类型本身,您有两个选择:

  1. 为父级声明手动序列化每个父级成员的类型创建自定义JsonConverter ,或者

  2. 创建一个自定义合约解析器,将成员转换为返回某些非空代理或包装对象的成员。

选项#2 更易于维护。以下合同解析器应该完成这项工作,包装每个成员的返回值,返回传入类型列表中指定的类型值以及所需的版本信息:

public class CustomContractResolver : DefaultContractResolver
{
    // Because contracts are cached, WrappedTypes must not be modified after construction.
    readonly HashSet<Type> WrappedTypes = new HashSet<Type>();

    public CustomContractResolver(IEnumerable<Type> wrappedTypes)
    {
        if (wrappedTypes == null)
            throw new ArgumentNullException();
        foreach (var type in wrappedTypes)
            WrappedTypes.Add(type);
    }

    class VersionWrapperProvider<T> : IValueProvider
    {
        readonly IValueProvider baseProvider;

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

        public object GetValue(object target)
        {
            return new VersionWrapper<T>(target, baseProvider);
        }

        public void SetValue(object target, object value) { }
    }

    class ReadOnlyVersionWrapperProvider<T> : IValueProvider
    {
        readonly IValueProvider baseProvider;

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

        public object GetValue(object target)
        {
            return new ReadOnlyVersionWrapper<T>(target, baseProvider);
        }

        public void SetValue(object target, object value) { }
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);
        if (WrappedTypes.Contains(property.PropertyType) 
            && !(member.DeclaringType.IsGenericType 
                && (member.DeclaringType.GetGenericTypeDefinition() == typeof(VersionWrapper<>) || member.DeclaringType.GetGenericTypeDefinition() == typeof(ReadOnlyVersionWrapper<>))))
        {
            var wrapperGenericType = (property.Writable ? typeof(VersionWrapper<>) : typeof(ReadOnlyVersionWrapper<>));
            var providerGenericType = (property.Writable ? typeof(VersionWrapperProvider<>) : typeof(ReadOnlyVersionWrapperProvider<>));
            var wrapperType = wrapperGenericType.MakeGenericType(new[] { property.PropertyType });
            var providerType = providerGenericType.MakeGenericType(new[] { property.PropertyType });
            property.PropertyType = wrapperType;
            property.ValueProvider = (IValueProvider)Activator.CreateInstance(providerType, property.ValueProvider);
            property.ObjectCreationHandling = ObjectCreationHandling.Reuse;
        }

        return property;
    }
}

internal class VersionWrapper<T>
{
    readonly object target;
    readonly IValueProvider baseProvider;

    public VersionWrapper(object target, IValueProvider baseProvider)
    {
        this.target = target;
        this.baseProvider = baseProvider;
    }

    public int Version { get { return 1; } }

    [JsonProperty(NullValueHandling = NullValueHandling.Include)]
    public T Object 
    {
        get
        {
            return (T)baseProvider.GetValue(target);
        }
        set
        {
            baseProvider.SetValue(target, value);
        }
    }
}

internal class ReadOnlyVersionWrapper<T>
{
    readonly object target;
    readonly IValueProvider baseProvider;

    public ReadOnlyVersionWrapper(object target, IValueProvider baseProvider)
    {
        this.target = target;
        this.baseProvider = baseProvider;
    }

    public int Version { get { return 1; } }

    [JsonProperty(NullValueHandling = NullValueHandling.Include)]
    public T Object
    {
        get
        {
            return (T)baseProvider.GetValue(target);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后按如下方式使用它来包装 type 的所有属性MyContent

static IContractResolver resolver = new CustomContractResolver(new[] { typeof(MyContent) });

// And later
var settings = new JsonSerializerSettings
{
    ContractResolver = resolver,
};
var json = JsonConvert.SerializeObject(wrap, Formatting.Indented, settings);
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 出于此处解释的性能原因,您应该静态缓存合约解析器。

  • VersionWrapperProvider<T>创建一个包装对象,其中包含必要的版本信息以及Object使用 Json.NET 自己的获取和设置基础值的代理属性IValueProvider.

    由于 Json.NET 不会设置预分配的引用属性的值,而是简单地使用反序列化的属性值填充它,因此 的 setter 本身有必要VersionWrapper<T>.Object在父级中设置该值。

  • 如果您的包装类型是多态的,则您可能需要检查中CreateProperty()是否有任何基本类型。property.PropertyTypeWrappedTypes

  • 应测试填充预先存在的Wrap使用。JsonConvert.PopulateObject

  • 当反序列化传递给参数化构造函数的属性时,此解决方案可能不起作用。 DefaultContractResolver.CreatePropertyFromConstructorParameter在这种情况下需要修改。

工作示例 .Net fiddle在这里