Search code examples
c#.netjsonjson.netjsonconverter

How to Serialize using JSON.NET and Ignore a Nullable Struct Value


I am trying to serialize a nullable struct using JSON.NET with a custom JsonConverter. I would like a null value to be ignored/omitted in the JSON output e.g. I want my JSON output below to be {} instead of {"Number":null}. How can this be achieved? Here is a minimal repro with a unit test with what I'm trying to achieve.

[Fact]
public void UnitTest()
{
    int? number = null;
    var json = JsonConvert.SerializeObject(
        new Entity { Number = new HasValue<int?>(number) },
        new JsonSerializerSettings()
        { 
            DefaultValueHandling = DefaultValueHandling.Ignore,
            NullValueHandling = NullValueHandling.Ignore
        });

    Assert.Equal("{}", json); // Fails because json = {"Number":null}
}

public class Entity
{
    [JsonConverter(typeof(NullJsonConverter))]
    public HasValue<int?>? Number { get; set; }
}

public struct HasValue<T>
{
    public HasValue(T value) => this.Value = value;
    public object Value { get; set; }
}

public class NullJsonConverter : JsonConverter
{
    public override bool CanRead => false;
    public override bool CanConvert(Type objectType) => true;
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var values = (HasValue<int?>)value;
        var objectValue = values.Value;
        if (objectValue == null)
        {
            // How can I skip writing this property?
        }
        else
        {
            var token = JToken.FromObject(objectValue, serializer);
            token.WriteTo(writer);
        }
    }
}

Solution

  • You have three problems here:

    1. As explained in this answer to Custom Json.NET converter should not serialize a property:

      A custom JsonConverter cannot prevent its value from being serialized, because the property name referring to it will already have been written out by the time the converter is invoked. In Json.NET's architecture it is the responsibility of the containing type to decide which of its properties to serialize; the value converter then decides how to serialize the value being written.

    2. NullValueHandling.Ignore is not working because the property Entity.Number is not null, it has a value, namely an allocated HasValue<int?> struct with a null inner value:

      Number = new HasValue<int?>(number) // Not Number = null
      
    3. Similarly DefaultValueHandling.Ignore is not working because default(HasValue<int?>?) has the same value as a null nullable value -- which, as mentioned above, differs from the value assigned to Number.

    So what are your options here?

    You could use conditional property serialization to suppress serialization of Number when it's value is a non-null but with a null inner value:

    public class Entity
    {
        [JsonConverter(typeof(NullJsonConverter))]
        public HasValue<int?>? Number { get; set; }
    
        public bool ShouldSerializeNumber() { return Number.HasValue && Number.Value.Value.HasValue; }
    }
    

    Demo fiddle #1 here.

    However, this design seems a little overcomplex -- you have a nullable containing a struct that encapsulates nullable containing an integer -- i.e. Nullable<HasValue<Nullable<int>>>. Do you really need both levels of nullable? If not, you could simply remove the outer Nullable<> and DefaultValueHandling will now just work:

    public class Entity
    {
        [JsonConverter(typeof(NullJsonConverter))]
        public HasValue<int?> Number { get; set; }
    }
    

    Demo fiddle #2 here.

    In both cases I generalized NullJsonConverter to handle all possible types T for HasValue<T> as follows:

    public struct HasValue<T> : IHasValue
    {
        // Had to convert to c# 4.0 syntax for dotnetfiddle
        T m_value;
        public HasValue(T value) { this.m_value = value; }
        public T Value { get { return m_value; } set { m_value = value; } }
    
        public object GetValue() { return Value; }
    }
    
    internal interface IHasValue
    {
        object GetValue();
    }
    
    public class NullJsonConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType) { throw new NotImplementedException(); }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var valueType = objectType.GetGenericArguments()[0];
            var valueValue = serializer.Deserialize(reader, valueType);
            return Activator.CreateInstance(objectType, valueValue);
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            serializer.Serialize(writer, ((IHasValue)value).GetValue());
        }
    }
    

    Specifically by:

    • Changing the Value property to be typed.
    • Adding a non-generic interface to access the value as an object during serialization.
    • Directly deserializing the inner value then invoking the parameterized constructor during deserialization.

    Thus you could apply [JsonConverter(typeof(NullJsonConverter))] to HasValue<T> itself if you prefer.

    You might also consider making your HasValue<T> struct be immutable for reasons explained in Why are mutable structs “evil”?.