Serializing null in JSON.NET
Asked Answered
M

1

48

When serializing arbitrary data via JSON.NET, any property that is null is written to the JSON as

"propertyName" : null

This is correct, of course.

However I have a requirement to automatically translate all nulls into the default empty value, e.g. null strings should become String.Empty, null int?s should become 0, null bool?s should be false, and so on.

NullValueHandling is not helpful, since I dont want to Ignore nulls, but neither do I want to Include them (Hmm, new feature?).

So I turned to implementing a custom JsonConverter.
While the implementation itself was a breeze, unfortunately this still didnt work - CanConvert() is never called for a property that has a null value, and therefore WriteJson() is not called either. Apparently nulls are automatically serialized directly into null, without the custom pipeline.

For example, here is a sample of a custom converter for null strings:

public class StringConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(string).IsAssignableFrom(objectType);
    }

    ...
    public override void WriteJson(JsonWriter writer, 
                object value, 
                JsonSerializer serializer)
    {
        string strValue = value as string;

        if (strValue == null)
        {
            writer.WriteValue(String.Empty);
        }
        else
        {
            writer.WriteValue(strValue);
        }
    }
}

Stepping through this in the debugger, I noted that neither of these methods are called for properties that have a null value.

Delving into JSON.NET's sourcecode, I found that (apparently, I didnt go into a lot of depth) there is a special case checking for nulls, and explictly calling .WriteNull().

For what it's worth, I did try implementing a custom JsonTextWriter and overriding the default .WriteNull() implementation...

public class NullJsonWriter : JsonTextWriter
{
    ... 
    public override void WriteNull()
    {
        this.WriteValue(String.Empty);
    }
}

However, this can't work well, since the WriteNull() method knows nothing about the underlying datatype. So sure, I can output "" for any null, but that doesnt work well for e.g. int, bool, etc.

So, my question - short of converting the entire data structure manually, is there any solution or workaround for this?

Miry answered 12/1, 2012 at 11:0 Comment(10)
I'm guessing the WriteNull() method is called internally within the JSON serialization process and you can't determine which value you are currently serializing?Krummhorn
The WriteNull method is called by the JsonSerializer when the property has a null value. To be accurate, the value I'm serializing is always null :), but yes there seems to be no way to know the underlying data type for which the null is being written.Miry
What's the point of using nullable types if you are just going to ignore null as a valid state of the object?Guimar
@32bitkid good question, but in this case the nullable types are used server side (e.g. in the model), but the client/receiving end/view does not handle the null state very well (yes, I would rather have the client fixed, but its a complicated situation). In any event, just because I start with nullable types, shouldnt mean it cant be serialized into something else...Miry
Is it possible to override any of the WriteValue(Nullable<T>) for your case where you want the default value for that type? Is the corresponding method called such as for a int? is WriteValue(Nullable<int>) called or does it directly flow to WriteNull()? JsonTextWriterFahey
After cursory look at the source code of Json.Net, I don't think you can do this easily without modifying its source code.Hyetology
@Fahey if(value==null) is pretty much the first case :(Guimar
Gosh I wish there was a way to serialize NULL when a JsonConverterAttribute has been specified on a property. I specifically need to modify output something other than "null" on nullable properties in my case too. Perhaps we can get a feature request going for this.Maishamaisie
json.codeplex.com/workitem/22508Maishamaisie
Omg TEN YEARS later.... ! And now there is this: learn.microsoft.com/en-us/dotnet/standard/serialization/… JsonConverter<T>.HandleNull now enables a custom converter to handle null... 🤦‍♀️Miry
G
27

Okay, I think I've come up with a solution (my first solution wasn't right at all, but then again I was on the train). You need to create a special contract resolver and a custom ValueProvider for Nullable types. Consider this:

public class NullableValueProvider : IValueProvider
{
    private readonly object _defaultValue;
    private readonly IValueProvider _underlyingValueProvider;


    public NullableValueProvider(MemberInfo memberInfo, Type underlyingType)
    {
        _underlyingValueProvider = new DynamicValueProvider(memberInfo);
        _defaultValue = Activator.CreateInstance(underlyingType);
    }

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

    public object GetValue(object target)
    {
        return _underlyingValueProvider.GetValue(target) ?? _defaultValue;
    }
}

public class SpecialContractResolver : DefaultContractResolver
{
    protected override IValueProvider CreateMemberValueProvider(MemberInfo member)
    {
        if(member.MemberType == MemberTypes.Property)
        {
            var pi = (PropertyInfo) member;
            if (pi.PropertyType.IsGenericType && pi.PropertyType.GetGenericTypeDefinition() == typeof (Nullable<>))
            {
                return new NullableValueProvider(member, pi.PropertyType.GetGenericArguments().First());
            }
        }
        else if(member.MemberType == MemberTypes.Field)
        {
            var fi = (FieldInfo) member;
            if(fi.FieldType.IsGenericType && fi.FieldType.GetGenericTypeDefinition() == typeof(Nullable<>))
                return new NullableValueProvider(member, fi.FieldType.GetGenericArguments().First());
        }

        return base.CreateMemberValueProvider(member);
    }
}

Then I tested it using:

class Foo
{
    public int? Int { get; set; }
    public bool? Boolean { get; set; }
    public int? IntField;
}

And the following case:

[TestFixture]
public class Tests
{
    [Test]
    public void Test()
    {
        var foo = new Foo();

        var settings = new JsonSerializerSettings { ContractResolver = new SpecialContractResolver() };

        Assert.AreEqual(
            JsonConvert.SerializeObject(foo, Formatting.None, settings), 
            "{\"IntField\":0,\"Int\":0,\"Boolean\":false}");
    }
}

Hopefully this helps a bit...

Edit – Better identification of the a Nullable<> type

Edit – Added support for fields as well as properties, also piggy-backing on top of the normal DynamicValueProvider to do most of the work, with updated test

Guimar answered 12/1, 2012 at 12:40 Comment(10)
I believe you can do Type.IsValueType if you want.Deach
@IanJacobs I figured it out by using GetGenericTypeDefinition() == typeof (Nullable<>).Guimar
Wow, this is... a bit more complicated than I expected. Especially for something so trivial... Anyway it will take me a while to plug this in and check, but it looks good! Thanks, in the meantime...Miry
Will also need to put in a special case for String, since it's not actually a Nullable<> type...Miry
@Miry Whoops, I forgot that you also needed to translate strings. But it would be implemented more or less the same way. You could create a new ValueProvider just for strings, or change the existing signature rather than passing in the generic type, just pass in the default value (and move the Activator.CreateInstance() up one level)Guimar
@Miry and I agree, it wasn't as intuitive as I thought it would be, but that was the only solution I could come up with that fits within the existing architecture of JSON.NET and doesn't require any changes to the library itself.Guimar
Yeah, from there its pretty simple to stretch to cover strings, I would just add another condition in there, pretty much in the same pattern. And hey, I'm not complaining about the complicated, just surprised... Thanks for this!Miry
Excellent, this worked brilliantly! I did make a few small changes, such as pulling out the generic parameter inside the valueprovider, and setting the default only on demand, but overall your solution with IValueProvider did the trick! Thanks a ton!Miry
I know we don't do this here but, after a neverending day : Thank you !Assimilate
Omg TEN YEARS later.... ! And now there is this: learn.microsoft.com/en-us/dotnet/standard/serialization/… JsonConverter<T>.HandleNull now enables a custom converter to handle null... 🤦‍♀️Miry

© 2022 - 2024 — McMap. All rights reserved.