How to default a null JSON property to an empty array during serialization with a List<T> property in JSON.NET?
Asked Answered
A

4

10

Currently I have JSON that either comes in via an HTTP call or is stored in a database but during server processing they are mapped to C# objects.

These objects have properties like public List<int> MyArray.

When the JSON contains MyArray:null I want the resulting property to be an empty List<T> instead of a null List<T> property.

The goal is that the object will "reserialize" to JSON as MyArray:[], thus either saving to the database or responding out via HTTP as an empty array instead of null.

That way, no matter what, the C# class is basically scrubbing and enforcing an empty array for any List<T> property that would otherwise be null and cause things to break in the browser side code (for example: cannot read property 'length' of null).

Is there a way that during the serialization/deserialization I can have any null value that is paired to a List<T> property become an empty array instead?

Approbation answered 5/8, 2014 at 20:57 Comment(0)
S
12

You could always lazy load an empty list if its null.

OR

Use the NullValueHandling option on the JsonDeserializer.

var settings = new JsonSerializerSettings();
settings.NullValueHandling = NullValueHandling.Ignore;

return JsonConvert.DeserializeObject<T>(json, settings);

http://james.newtonking.com/json/help/index.html?topic=html/SerializationSettings.htm

Sufficiency answered 5/8, 2014 at 21:4 Comment(1)
After looking at the alternatives I am going to go with your original suggestion and just define my own getters, if you get around to it you could drop that code sample back in for future readers.Approbation
W
9

I was going to suggest using a custom JsonConverter to solve this, but a converter will not get called for null values. Instead, you will need to use a custom IContractResolver in combination with a custom IValueProvider. Here is the code you would need (inspired by this answer):

class NullToEmptyListResolver : DefaultContractResolver
{
    protected override IValueProvider CreateMemberValueProvider(MemberInfo member)
    {
        IValueProvider provider = base.CreateMemberValueProvider(member);

        if (member.MemberType == MemberTypes.Property)
        {
            Type propType = ((PropertyInfo)member).PropertyType;
            if (propType.IsGenericType && 
                propType.GetGenericTypeDefinition() == typeof(List<>))
            {
                return new EmptyListValueProvider(provider, propType);
            }
        }

        return provider;
    }

    class EmptyListValueProvider : IValueProvider
    {
        private IValueProvider innerProvider;
        private object defaultValue;

        public EmptyListValueProvider(IValueProvider innerProvider, Type listType)
        {
            this.innerProvider = innerProvider;
            defaultValue = Activator.CreateInstance(listType);
        }

        public void SetValue(object target, object value)
        {
            innerProvider.SetValue(target, value ?? defaultValue);
        }

        public object GetValue(object target)
        {
            return innerProvider.GetValue(target) ?? defaultValue;
        }
    }
}

Here is a demo which shows how to use the resolver:

class Program
{
    static void Main(string[] args)
    {
        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.ContractResolver = new NullToEmptyListResolver();
        settings.ObjectCreationHandling = ObjectCreationHandling.Replace;
        settings.Formatting = Formatting.Indented;

        Console.WriteLine("Serializing object with null lists...");
        Foo foo = new Foo();
        string json = JsonConvert.SerializeObject(foo, settings);
        Console.WriteLine(json);
        Console.WriteLine();

        Console.WriteLine("Deserializing JSON with null lists...");
        json = @"{ ""IntList"" : null, ""StringList"" : null }";
        foo = JsonConvert.DeserializeObject<Foo>(json, settings);
        Console.WriteLine("IntList size: " + foo.IntList.Count);
        Console.WriteLine("StringList size: " + foo.StringList.Count);
    }
}

class Foo
{
    public List<int> IntList { get; set; }
    public List<string> StringList { get; set; }
}

Output:

Serializing object with null lists...
{
  "IntList": [],
  "StringList": []
}

Deserializing JSON with null lists...
IntList size: 0
StringList size: 0
Weiweibel answered 6/8, 2014 at 0:8 Comment(3)
This doesn't seem to deserialize when the list properties exist/have any data; in that case they get deserialized as nulls. Your example doesn't round trip (try deserializing the result of the original serialization). Any ideas? I really liked this approach but it's just not working -- I need an Array in my json source that is null to deserialize to an empty list, but one that has data to deserialize to that list with said data.Deforce
@Deforce You're right-- you need to set ObjectCreationHandling to Replace for the round-trip to work correctly. I've amended my answer. Here is a fiddle to prove it works once this setting is added: dotnetfiddle.net/wsEyzeWeiweibel
Cool, thanks! I thought I was going crazy originally until I tried your code verbatim and then attempted the round trip. Much appreciatedDeforce
C
9

The following property will have empty collection assigned to it after deserialization instead of null in both cases: when the property is omitted in JSON or when set to null explicitly:

class A
{
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public IEnumerable<int> Prop { get; set; } = new List<int>();
}
Comedo answered 11/1, 2019 at 2:56 Comment(1)
what if we move = new List<int>(); to constructor, would it still works?Spragens
B
3

To use Brian Rogers solution in a .net core solution you need a slight modification to access the "IsGenericType" property as it moved from Type to TypeInfo.

The answer marked as correct (setting NullValueHandling) was not working for me, it just ignores the property if it is null.

Full code for .net core:

using System;
using System.Collections.Generic;
using Newtonsoft.Json.Serialization;
using System.Reflection;

public class NullToEmptyListResolver : DefaultContractResolver
{
    protected override IValueProvider CreateMemberValueProvider(MemberInfo member)
    {
        IValueProvider provider = base.CreateMemberValueProvider(member);

        if (member.MemberType == MemberTypes.Property)
        {
            Type propType = ((PropertyInfo)member).PropertyType;
            TypeInfo propTypeInfo = propType.GetTypeInfo();
            if (propTypeInfo.IsGenericType &&
                propType.GetGenericTypeDefinition() == typeof(List<>))
            {
                return new EmptyListValueProvider(provider, propType);
            }
        }

        return provider;
    }

    class EmptyListValueProvider : IValueProvider
    {
        private IValueProvider innerProvider;
        private object defaultValue;

        public EmptyListValueProvider(IValueProvider innerProvider, Type listType)
        {
            this.innerProvider = innerProvider;
            defaultValue = Activator.CreateInstance(listType);
        }

        public void SetValue(object target, object value)
        {
            innerProvider.SetValue(target, value ?? defaultValue);
        }

        public object GetValue(object target)
        {
            return innerProvider.GetValue(target) ?? defaultValue;
        }
    }
}
Bedizen answered 24/2, 2017 at 15:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.