Search code examples
c#json.netjson.netdeserialization

C# Newtonsoft JSON Deserialization: Empty list value represented as "{}" instead of "[]", throws exception


We are currently supporting an API integration with a 3rd party that transmits responses via JSON. We are using C# .NET and Newtonsoft JSON (version 13.0.1) to handle serialization of requests and deserialization of responses.

However, the 3rd party is serializing empty lists in some of their responses in a manner that we aren't expecting. Here's an example.

When the list parameter contains values

{
    "code": 0,
    "msg": "OK",
    "info": [
        {/* Line Item 1 */}, 
        {/* Line Item 2*/}
    ]
}

(with Line Item 1 and 2 replaced with valid JSON for another object type)

When the list parameter is empty

{
    "code": 0,
    "msg": "OK",
    "info": {}
}

As far as I can tell, when a list/collection parameter is supposed to be null or empty, it's either supposed to be "[]" or just missing entirely. Both would be fine from a deserialization standpoint. Instead, here's the exception we get back.

Newtonsoft.Json.JsonSerializationException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.IList`1[GenericResponseLineItem]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly. To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.

Here's how I'm currently defining the class in C#

[Serializable]
public class GenericNamedResponseObject
{
    [JsonProperty("code")]
    public int? Code { get; set; }

    [JsonProperty("msg")]
    public string ErrorMessage { get; set; }
    
    private IList<GenericResponseLineItem> _info;

    [JsonProperty("info")]
    public IList<GenericResponseLineItem> Info
    {
        get => _info ?? (_info = new List<GenericResponseLineItem>());
        set => _info = value;
    }
    
}

Current Workaround

At the moment, I've gotten a workaround in place that does a try/catch around deserializing my GenericNamedResponseObject, then tries to deserialize a GenericNamedResponseAlternateFormatting object that I've written, which omits the Info parameter entirely.

This works, but it's an ugly hack, and we're seeing even more responses from the 3rd party that are similarly formatted.

Is there a property in Newtonsoft JSON that would accept both "[]" and "{}" as empty list representations during deserialization? The 3rd party isn't being consistent at all with this, as some response types are deserializing cleanly, and some aren't.

Or if I need to write a custom deserializer, how would I handle this properly?


Solution

  • This can be solved by using a custom JsonConverter similar to the one found in this answer to How to handle both a single item and an array for the same property using JSON.net. In your case, you want to return an empty list if you get anything other than an array in the JSON. Here is a converter that does that:

    public class SafeArrayConverter<T> : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return (objectType == typeof(List<T>));
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JToken token = JToken.Load(reader);
            if (token.Type == JTokenType.Array)
            {
                return token.ToObject<List<T>>();
            }
            return new List<T>();
        }
    
        public override bool CanWrite => false;
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    

    To use the converter, you just need to add a [JsonConverter] attribute to the Info property in your GenericNamedResponseObject class, like this:

        [JsonProperty("info")]
        [JsonConverter(typeof(SafeArrayConverter<GenericResponseLineItem>))]
        public IList<GenericResponseLineItem> Info
        {
            get => _info ?? (_info = new List<GenericResponseLineItem>());
            set => _info = value;
        }
    

    Here is a working demo: https://dotnetfiddle.net/HnQTPM