Serializing null in JSON.NET

52,686

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

Share:
52,686

Related videos on Youtube

AviD
Author by

AviD

Security expert and experienced Windows programmer

Updated on July 09, 2022

Comments

  • AviD
    AviD over 1 year

    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?

    • Samuel Slade
      Samuel Slade about 12 years
      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?
    • AviD
      AviD about 12 years
      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.
    • J. Holmes
      J. Holmes about 12 years
      What's the point of using nullable types if you are just going to ignore null as a valid state of the object?
    • AviD
      AviD about 12 years
      @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...
    • ShelbyZ
      ShelbyZ about 12 years
      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()? JsonTextWriter
    • svick
      svick about 12 years
      After cursory look at the source code of Json.Net, I don't think you can do this easily without modifying its source code.
    • J. Holmes
      J. Holmes about 12 years
      @ShelbyZ if(value==null) is pretty much the first case :(
    • Brian Chavez
      Brian Chavez almost 12 years
      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.
    • Brian Chavez
      Brian Chavez almost 12 years
  • Ian Jacobs
    Ian Jacobs about 12 years
    I believe you can do Type.IsValueType if you want.
  • J. Holmes
    J. Holmes about 12 years
    @IanJacobs I figured it out by using GetGenericTypeDefinition() == typeof (Nullable<>).
  • AviD
    AviD about 12 years
    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...
  • AviD
    AviD about 12 years
    Will also need to put in a special case for String, since it's not actually a Nullable<> type...
  • J. Holmes
    J. Holmes about 12 years
    @AviD 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)
  • J. Holmes
    J. Holmes about 12 years
    @AviD 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.
  • AviD
    AviD about 12 years
    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!
  • AviD
    AviD about 12 years
    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!
  • Guillaume Beauvois
    Guillaume Beauvois almost 8 years
    I know we don't do this here but, after a neverending day : Thank you !