c# - 如何强制为空值调用 JsonConverter.WriteJson()

标签 c# json.net

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

当属性不为 null 时我得到的结果:

{"Prop":{"Version":1, "Object":{"Content":"abc"}}}

当它为 null 时我得到:

{"Prop":null}

当它为 null 时我想要:

{"Prop":{"Version":1, "Object":null}}

由于从未为空值调用 WriteJson,我没有机会控制此行为。有什么办法可以强制执行此操作吗?

请注意,我想知道这是否可能与转换器或契约(Contract)解析器有关,我不能/不想更改 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;}
}

最佳答案

目前无法进行 Json.NET 调用 JsonConverter.WriteJson()对于 null值(value)。这可以在 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

所以如果你需要翻译null成员到非空 JSON 值但不能修改类型本身,您有两个选择:

  1. 创建 custom JsonConverter 对于声明成员类型的父级,手动序列化每个父级,或者

  2. 创建 custom contract resolver将成员转换为返回一些非空代理或包装对象的成员。

选项 #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);
        }
    }
}

然后如下使用它来包装类型为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);

注意事项:

  • 出于性能原因,您应该静态缓存契约解析器 here .

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

    因为 Json.NET 不会设置预分配引用属性的值,而是简单地用反序列化的属性值填充它,所以 VersionWrapper<T>.Object 的 setter 是必需的自己在父级中设置值。

  • 如果你的包装类型是多态的,在 CreateProperty()您可能需要检查 property.PropertyType 的任何基本类型在 WrappedTypes .

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

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

工作示例 .Net fiddle here .

关于c# - 如何强制为空值调用 JsonConverter.WriteJson(),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52518593/

相关文章:

javascript - 使用 json.parse 的字符无效

c# - 在 C# 中通过 POST 发送 JSON 并接收返回的 JSON?

c# - 使用 LibGit2Sharp 以编程方式删除本地存储库

c# - WPF 中类似 Acrobat 的 PopUp

c# - 无法在 MVC 5 中为 Html.DropDownList 设置默认值

c# - 如何按值对 JObject 进行排序

c# - 在重用对象和替换数组的地方填充对象?

c# - 从内存中清除 C# 字符串

c# - 动态生成的 IL 中的值类型转换

c# - 使用 Json.Net 的 JObject.FromObject 方法进行序列化时,我可以使用自定义 ContractResolver 吗?