5

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;}
}
dbc
  • 104,963
  • 20
  • 228
  • 340
David S.
  • 5,965
  • 2
  • 40
  • 77
  • Might want to look at [`JsonSerializerSettings`](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonSerializerSettings_NullValueHandling.htm)... – Heretic Monkey Sep 26 '18 at 13:29
  • @HereticMonkey Even with `NullValueHandling.Include`, it does not call `WriteJson`. – David S. Sep 26 '18 at 13:38
  • 1
    Yeah, looks like it just doesn't do that. See [Json.NET not call my custom converter if that value is null](https://stackoverflow.com/q/24113900) and [NullValueHandling.Ignore with JsonConverter::WriteJson](https://stackoverflow.com/a/33248670) – Heretic Monkey Sep 26 '18 at 13:43
  • I am trying to work around by overriding something from DefaultContractResolver to convert the property into a wrapper type on the fly that will never be null, but I'm struggling there because I'm not familiar enough with it that I know how to or if it's even possible. I tried override contractResolver.CreateProperty and set propertytype and defaultvalue handling there but doesn't seem to work. – David S. Sep 26 '18 at 15:03

1 Answers1

10

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Wow, this works perfectly! I agree option 2 is superior. – David S. Sep 27 '18 at 09:12
  • unfortunately I discovered a bug in your test on net fiddle, you assert `wrap.Prop.Content == wrap.Prop.Content` but it should be `wrap.Prop.Content == wrap2.Prop.Content`. And now the assert fails with null exception for the objects with non-null Prop. It seems the Prop is never set to the actual content. Any idea where to fix this? – David S. Sep 27 '18 at 11:37
  • 1
    Looks like creating a `JsonConverter` for `bool CanConvert(Type objectType) => objectType == typeof(VersionWrapper);` and `bool CanWrite => false;` and registering it in the settings, solves the issue. Also, it looks like it's not necessary to set `property.PropertyType = wrapperType;` inside `CreateProperty`. I wonder if it can be done only inside the converter, or in a more elegant way. – David S. Sep 27 '18 at 13:34
  • 1
    @DavidS. - OK, I think I found the issue. The problem was that Json.NET will not set back a pre-allocated property value after populating it, thus the underlying value never got set. Fixing that required me to change the `VersionWrapper` to include the `IValueProvider` and call the setter & getter itself. Seems like you found a different fix though, be interesting to see what it was. – dbc Sep 27 '18 at 13:41
  • 1
    Thanks!! Here is my other fix: https://dotnetfiddle.net/4easi0 not necessarily maintainable, I might prefer your solution. Maybe there are some performance considerations here as well – David S. Sep 27 '18 at 14:00
  • 1
    Right, by using a converter and returning a fresh value, you force Json.NET to set the value back. Using the converter also renders the value of `property.PropertyType` irrelevant. One improvement would be to construct and set the converter on `property.Converter` inside `CreateProperty()`. – dbc Sep 27 '18 at 14:05