7

I am reading in a list of objects from JSON using this call:

Rootobject userInfo = JsonConvert.DeserializeObject<Rootobject>(File.ReadAllText(strFileName));

But I get an exception Cannot deserialize the current JSON array. If one of the arrays within one of the class objects is empty. As long as there is data everything works.

Here is an example of JSON that is tripping up the Deserializer:

This is normal type of data for the Venue object:

"venue":  {
            "venue_id":  696895,
            "venue_name":  "Blackfinn Ameripub",
            "venue_slug":  "blackfinn-ameripub",
            "primary_category":  "Food",
            "parent_category_id":  "4d4b7105d754a06374d81259",
            "categories":  {
                "count":  1,
                "items":  [
                            {
                                "category_name":  "American Restaurant",
                                "category_id":  "4bf58dd8d48988d14e941735",
                                "is_primary":  true
                            }
                        ]
            },
            "is_verified":  false
        },

And here is what is causing the exception, an empty array:

"venue":  [

        ],

I have tried using the JsonSerializerSettings options including DefaultValueHandling, NullValueHandling and MissingMemberHandling but none of them seem to prevent the error.

Any idea how to deserialize the JSON and just ignore any empty arrays within the data? I'd like this to handle any empty arrays not just the example above for the object Venue.

New issue was found - 03/17/2018 <<

Hi, the converter below has been working perfectly but the server I am getting my json responses from threw another challenge. JSON.NET has had no problem retrieving this type of data:

 "toasts":  {
                "total_count":  1,
                "count":  1,
                "auth_toast":  false,
                "items":  [
                              {
                                  "uid":  3250810,
                                  "user":  {
                                               "uid":  3250810,
                                               "account_type":  "user",
                                               "venue_details":  [

                                                                 ],
                                               "brewery_details":  [

                                                                   ]
                                           },
                                  "like_id":  485242625,
                                  "like_owner":  false,
                                  "created_at":  "Wed, 07 Mar 2018 07:54:38 +0000"
                              }
                          ]
            },

Specifically the section that has venue_details. 99% of the responses come back with venue_details in this format:

 "venue_details":  [

                   ],

But then I get this format suddenly:

 "toasts":  {
                "total_count":  1,
                "count":  1,
                "auth_toast":  false,
                "items":  [
                              {
                                  "uid":  4765742,
                                  "user":  {
                                               "uid":  4765742,
                                               "account_type":  "venue",
                                               "venue_details":  {
                                                                     "venue_id":  4759473
                                                                 },
                                               "brewery_details":  [

                                                                   ],
                                               "user_link":  "https://untappd.com/venue/4759473"
                                           },
                                  "like_id":  488655942,
                                  "like_owner":  false,
                                  "created_at":  "Fri, 16 Mar 2018 16:47:10 +0000"
                              }
                          ]
            },

Notice how venue_details now has a value and includes a venue_id.
So instead venue_details ends up looking like an object instead of an array. This ends up giving this exception:

JsonSerializationException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[System.Object]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.

In the converter code provided, that exception happens in this line with *s next to it:

public abstract class IgnoreUnexpectedArraysConverterBase : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var contract = serializer.ContractResolver.ResolveContract(objectType);
        if (!(contract is JsonObjectContract))
        {
            throw new JsonSerializationException(string.Format("{0} is not a JSON object", objectType));
        }

        do
        {
            if (reader.TokenType == JsonToken.Null)
                return null;
            else if (reader.TokenType == JsonToken.Comment)
                continue;
            else if (reader.TokenType == JsonToken.StartArray)
            {
                var array = JArray.Load(reader);
                if (array.Count > 0)
                    throw new JsonSerializationException(string.Format("Array was not empty."));
                return existingValue ?? contract.DefaultCreator();
            }
            else if (reader.TokenType == JsonToken.StartObject)
            {
                // Prevent infinite recursion by using Populate()
                existingValue = existingValue ?? contract.DefaultCreator();
            *** serializer.Populate(reader, existingValue); ***
                return existingValue;

Any ideas how to add this additional handling to account for a flip like this between the JSON returning an object instead of an array?

Thanks, Rick

Rick Engle
  • 103
  • 1
  • 10

1 Answers1

7

Your problem is not that you need to ignore empty arrays. If the "items" array were empty, there would be no problem:

"items":  [],

Instead your problem is as follows. The JSON standard supports two types of container:

  • The array, which is an ordered collection of values. An array begins with [ (left bracket) and ends with ] (right bracket). Values are separated by , (comma).

  • The object, which is an unordered set of name/value pairs. An object begins with { (left brace) and ends with } (right brace).

For some reason the server is returning an empty array in place of a null object. If Json.NET expects to encounter a JSON object but instead encounters a JSON array, it will throw the Cannot deserialize the current JSON array exception you are seeing.

You might consider asking whoever generated the JSON to fix their JSON output, but in the meantime, you can use the following converters to skip unexpected arrays when deserializing objects:

public class IgnoreUnexpectedArraysConverter<T> : IgnoreUnexpectedArraysConverterBase
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }
}

public class IgnoreUnexpectedArraysConverter : IgnoreUnexpectedArraysConverterBase
{
    readonly IContractResolver resolver;

    public IgnoreUnexpectedArraysConverter(IContractResolver resolver)
    {
        if (resolver == null)
            throw new ArgumentNullException();
        this.resolver = resolver;
    }

    public override bool CanConvert(Type objectType)
    {
        if (objectType.IsPrimitive || objectType == typeof(string))
            return false;
        return resolver.ResolveContract(objectType) is JsonObjectContract;
    }
}

public abstract class IgnoreUnexpectedArraysConverterBase : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var contract = serializer.ContractResolver.ResolveContract(objectType);
        if (!(contract is JsonObjectContract))
        {
            throw new JsonSerializationException(string.Format("{0} is not a JSON object", objectType));
        }

        do
        {
            if (reader.TokenType == JsonToken.Null)
                return null;
            else if (reader.TokenType == JsonToken.Comment)
                continue;
            else if (reader.TokenType == JsonToken.StartArray)
            {
                var array = JArray.Load(reader);
                if (array.Count > 0)
                    throw new JsonSerializationException(string.Format("Array was not empty."));
                return null;
            }
            else if (reader.TokenType == JsonToken.StartObject)
            {
                // Prevent infinite recursion by using Populate()
                existingValue = existingValue ?? contract.DefaultCreator();
                serializer.Populate(reader, existingValue);
                return existingValue;
            }
            else
            {
                throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
            }
        }
        while (reader.Read());
        throw new JsonSerializationException("Unexpected end of JSON.");
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Then, if empty arrays can appear in only one place in the object graph, you can add the converter to your model as follows:

public class Rootobject
{
    [JsonConverter(typeof(IgnoreUnexpectedArraysConverter<Venue>))]
    public Venue venue { get; set; }
}

But if, as you say, any object might be replaced with an empty array, you can use the non-generic IgnoreUnexpectedArraysConverter for all object types:

var resolver = new DefaultContractResolver(); // Cache for performance
var settings = new JsonSerializerSettings
{
    ContractResolver = resolver,
    Converters = { new IgnoreUnexpectedArraysConverter(resolver) },
};
var userInfo = JsonConvert.DeserializeObject<Rootobject>(jsonString, settings);

Notes:

  • The converter does not work with the TypeNameHandling or PreserveReferencesHandling settings.

  • The converter assumes that the object being deserialized has a default constructor. It the object has a parameterized constructor you will need to create a hardcoded converter to allocate and populate the object.

  • The converter throws an exception if the array is not empty, to ensure there is no data loss in the event of incorrect assumptions about the structure of the JSON. Sometimes servers will write a single object in place of a one-object array, and an array when there are zero, two or more objects. If you are also in that situation (e.g. for the "items" array) see How to handle both a single item and an array for the same property using JSON.net.

  • If you want the converter to return a default object instead of null when encountering an array, change it as follows:

    else if (reader.TokenType == JsonToken.StartArray)
    {
        var array = JArray.Load(reader);
        if (array.Count > 0)
            throw new JsonSerializationException(string.Format("Array was not empty."));
        return existingValue ?? contract.DefaultCreator();
    }
    

Working sample .Net fiddle.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • dbc, that was a phenomenal answer, it completely fixed up my issue and I went for the 2nd/any-object solution you suggested. I used that code inside of LINQPad, ran it against the full JSON data and sure enough only the 1 Venue object that was missing now came back null and all other objects populated correctly. – Rick Engle Feb 14 '18 at 21:30
  • One more thing, could the converter be enhanced so that instead of the object being completely null, it could create a new instance of it instead so the Venue object would be present for that item in the model along with its properties but the properties would be null? – Rick Engle Feb 14 '18 at 21:38
  • @RickEngle - updated with option to return a default instance in the event of an empty array. – dbc Feb 14 '18 at 22:41
  • Hi, I found another issue with deserialization of Json responses where an item is suddenly being returned as an object instead of an empty array and gives me this error in the converter: sonSerializationException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[System.Object]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly. I edited my post above with full details. I would love advice on how to expand the converter to handle this edge case. – Rick Engle Mar 17 '18 at 14:23
  • @RickEngle - the rule on stack overflow is to ask [one question per post?](https://meta.stackexchange.com/q/222735/344280), so you should ask another question. But see [How to handle both a single item and an array for the same property using JSON.net](https://stackoverflow.com/q/18994685/3744182) first. – dbc Mar 17 '18 at 17:33
  • sorry dbc, I was worried I would end up with a duplicate post. I actually found a simpler solution. I have been using json2csharp.com to create C# classes from the json but in this case the object model was not fully formed since there wasn't any sample data so my property that was crashing was this: public List venue_details { get; set; } and once I had data and reran the class generator I got this: public VenueDetails venue_details { get; set; }. Once I had that new property and the VenueDetails class the error went away! – Rick Engle Mar 17 '18 at 23:57