Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
540 views
in Technique[技术] by (71.8m points)

c# - How to force JsonConverter.WriteJson() to be called for a null value

I want to wrap some properties in a JSON object with some metadata, regardless if it's null or not. However, my custom JsonConverter.WriteJson override is not called in case the property is null.

What I get when property is not null:

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

What I get when it's null:

{"Prop":null}

What I want when it's null:

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

Due to WriteJson never being called for null values, I do not get the opportunity to control this behavior. Is there any way to force this?

Note that I want to know if this is possible to do with e.g converters or contractresolvers, I can't/don't want to change the MyContent or Wrap classes (see below).

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;}
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

There is no way currently to make Json.NET call JsonConverter.WriteJson() for a null value. This can be seen in JsonSerializerInternalWriter.SerializeValue(...) which immediately writes a null and returns for a null incoming value:

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

So if you need to translate null member(s) to non-null JSON value(s) but cannot modify the types themselves, you have two options:

  1. Create a custom JsonConverter for the parent declaring type(s) of the member(s) that serializes every parent manually, OR

  2. Create a custom contract resolver that translates the member(s) to ones returning some non-null surrogate or wrapper object.

Option #2 is more maintainable. The following contract resolver should do the job, wrapping the returned value of every member returning a value of the type(s) specified in the incoming list of types with the required version information:

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);
        }
    }
}

Then use it as follows to wrap all properties of 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);

Notes:

  • You should statically cache the contract resolver for performance reasons explained here.

  • VersionWrapperProvider<T> creates a wrapper object with the necessary version information as well as a surrogate Object property that gets and sets the underlying value using Json.NET's own IValueProvider.

    Because Json.NET does not set back the value of a pre-allocated reference property, but instead simply populates it with the deserialized property values, it is necessary for the setter of VersionWrapper<T>.Object to itself set the value in the parent.

  • If your wrapped types are polymorphic, in CreateProperty() you may need to check whether any of the base types of property.PropertyType are in WrappedTypes.

  • Populating a pre-existing Wrap using JsonConvert.PopulateObject should be tested.

  • This solution may not work when deserializing properties passed to parameterized constructors. DefaultContractResolver.CreatePropertyFromConstructorParameter would need modification in such a situation.

Working sample .Net fiddle here.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...