0
votes

I'm often creating C# classes for Json data responses using http://json2csharp.com and what I find often is that if a specific snapshot of the Json I am plugging into the class generator is missing data, then I don't end up with the right class properties to handle the data which can end up causing an unhandled deserialization error. Here is an example:

    "toasts": {
        "total_count": 1,
        "count": 1,
        "auth_toast": false,
        "items": [
            {
                "uid": 3250810,
                "user": {
                    "uid": 3250810,
                    "user_name": "jdoe",
                    "first_name": "Jane",
                    "last_name": "Doe",
                    "bio": "",
                    "location": "",
                    "relationship": "friends",
                    "user_avatar": "",
                    "account_type": "user",
                    "venue_details": [
                    ],
                    "brewery_details": [
                    ]
                },
                "like_id": 488764809,
                "like_owner": false,
                "created_at": "Fri, 16 Mar 2018 20:35:44 +0000"
            }
        ]
    },

In that example, there is no data under "venue_details:" so no properties are discovered and the C# class generated looks like this:

    public class ViewCheckinID_Toasts
    {
        public int total_count { get; set; }
        public int count { get; set; }
        public bool auth_toast { get; set; }
        public List<ViewCheckinID_Item2> items { get; set; }
    }


    public class ViewCheckinID_Item2
    {
        public int uid { get; set; }
        public ViewCheckinID_User2 user { get; set; }
        public int like_id { get; set; }
        public bool like_owner { get; set; }
        public string created_at { get; set; }
    }

public class ViewCheckinID_User2
{
    public int uid { get; set; }
    public string user_name { get; set; }
    public string first_name { get; set; }
    public string last_name { get; set; }
    public string bio { get; set; }
    public string location { get; set; }
    public string relationship { get; set; }
    public string user_avatar { get; set; }
    public string account_type { get; set; }
    public object[] venue_details { get; set; }
    public object[] brewery_details { get; set; }
    public string user_link { get; set; }
}

Notice that towards the end of the class, the property generated was: public object[] venue_details { get; set; }

What happens though if the Json data suddenly ends up having underlying data within venue_details, I get an exception. Here is a snip of the Json:

                        "venue_details":  {
                            "venue_id":  329118
                        },

So now its not an array but actually a string property. That ends up causing the deserialization error of:

JsonSerializationException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Object[]' 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. Path 'response.checkin.toasts.items[0].user.venue_details.venue_id', line 143, position 20.

If I take that new Json and run it through the C# classes generator again, I then get this fixed up property and the error then goes away:

    public class ViewCheckinID_User2
    {
        public int uid { get; set; }
        public string user_name { get; set; }
        public string first_name { get; set; }
        public string last_name { get; set; }
        public string bio { get; set; }
        public string location { get; set; }
        public string relationship { get; set; }
        public string user_avatar { get; set; }
        public string account_type { get; set; }
        public ViewCheckinID_VenueDetails venue_details { get; set; }
        public object[] brewery_details { get; set; }
        public string user_link { get; set; }
    }

    public class ViewCheckinID_VenueDetails
    {
        public int venue_id { get; set; }
    }

What'd like to do is be able to defend against this error. I have been using these helper classes for deserialization but it doesn't handle that case:

    string strFileName = @"C:\Users\rick\Documents\files\dev\Explorer\DataModels\ricke_Checkin_View_CheckinID.json";


    var resolver = new DefaultContractResolver(); // Cache for performance
    var Serializersettings = new JsonSerializerSettings
    {
        ContractResolver = resolver,
        Converters = { new IgnoreUnexpectedArraysConverter(resolver) },
    };
    ViewCheckinID_RootObject checkinInfo = JsonConvert.DeserializeObject<ViewCheckinID_RootObject>(File.ReadAllText(strFileName), Serializersettings);

    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 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;
                }
                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();
        }
    }


    public class SingleValueArrayConverter<T> : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            object retVal = new Object();
            if (reader.TokenType == JsonToken.StartObject)
            {
                T instance = (T)serializer.Deserialize(reader, typeof(T));
                retVal = new List<T>() { instance };
            } else if (reader.TokenType == JsonToken.StartArray) {
                retVal = serializer.Deserialize(reader, objectType);
            }
            return retVal;
        }

        public override bool CanConvert(Type objectType)
        {
            return true;
        }
    }

If you see in the helper class IgnoreUnexpectedArraysConverterBase, the line that starts with *** is where the deserialization exception had occurred.

So now that I laid all of that information out, my questions are:

  • How can I add a handler for the situation where a class property is
    expecting an Object, because no data was found when generating the
    classes and suddenly there is data there?

  • Is there a way to add a catch-all handler so that if any exception
    happens trying to deserialize a property that it is just simply
    ignored and a placeholder value like a null or empty string is
    returned?

I seem to keep getting into an endless situation of getting new Json responses that the class definitions didn't properly anticipate and my app crashes with yet another deserializion error. I would like the deserialization to be resilient enough to defend against any kind of unanticipated data and if it does run into an issue, fail gracefully and let teh app continue running.

1
Primarily Opinion Based - Many good questions generate some degree of opinion based on expert experience, but answers to this question will tend to be almost entirely based on opinions, rather than facts, references, or specific expertise. Please read What types of questions should I avoid asking? before attempting to ask more questions.user177800

1 Answers

0
votes

I checked out your link json2chsarp. If you had enabled the "Make all properties optional" you would probably not have faced this issue. I had the same with respect to Azure and Google's OCR JSON strings. I had to do a lot of rewrites to my code.

enter image description here

Formatted JSON.

{
   "toasts":[
      {
         "total_count":1,
         "count":1,
         "auth_toast":false,
         "items":[
            {
               "uid":3250810,
               "user":{
                  "uid":3250810,
                  "user_name":"jdoe",
                  "first_name":"Jane",
                  "last_name":"Doe",
                  "bio":"",
                  "location":"",
                  "relationship":"friends",
                  "user_avatar":"",
                  "account_type":"user",
                  "venue_details":[

                  ],
                  "brewery_details":[

                  ]
               },
               "like_id":488764809,
               "like_owner":false,
               "created_at":"Fri, 16 Mar 2018 20:35:44 +0000"
            }
         ]
      }
   ]
}

Auto generated code from quicktype.

// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do:
//
//    using QuickType;
//
//    var welcome = Welcome.FromJson(jsonString);

namespace QuickType
{
    using System;
    using System.Collections.Generic;

    using System.Globalization;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Converters;

    public partial class Welcome
    {
        [JsonProperty("toasts", NullValueHandling = NullValueHandling.Ignore)]
        public Toast[] Toasts { get; set; }
    }

    public partial class Toast
    {
        [JsonProperty("total_count", NullValueHandling = NullValueHandling.Ignore)]
        public long? TotalCount { get; set; }

        [JsonProperty("count", NullValueHandling = NullValueHandling.Ignore)]
        public long? Count { get; set; }

        [JsonProperty("auth_toast", NullValueHandling = NullValueHandling.Ignore)]
        public bool? AuthToast { get; set; }

        [JsonProperty("items", NullValueHandling = NullValueHandling.Ignore)]
        public Item[] Items { get; set; }
    }

    public partial class Item
    {
        [JsonProperty("uid", NullValueHandling = NullValueHandling.Ignore)]
        public long? Uid { get; set; }

        [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)]
        public User User { get; set; }

        [JsonProperty("like_id", NullValueHandling = NullValueHandling.Ignore)]
        public long? LikeId { get; set; }

        [JsonProperty("like_owner", NullValueHandling = NullValueHandling.Ignore)]
        public bool? LikeOwner { get; set; }

        [JsonProperty("created_at", NullValueHandling = NullValueHandling.Ignore)]
        public string CreatedAt { get; set; }
    }

    public partial class User
    {
        [JsonProperty("uid", NullValueHandling = NullValueHandling.Ignore)]
        public long? Uid { get; set; }

        [JsonProperty("user_name", NullValueHandling = NullValueHandling.Ignore)]
        public string UserName { get; set; }

        [JsonProperty("first_name", NullValueHandling = NullValueHandling.Ignore)]
        public string FirstName { get; set; }

        [JsonProperty("last_name", NullValueHandling = NullValueHandling.Ignore)]
        public string LastName { get; set; }

        [JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)]
        public string Bio { get; set; }

        [JsonProperty("location", NullValueHandling = NullValueHandling.Ignore)]
        public string Location { get; set; }

        [JsonProperty("relationship", NullValueHandling = NullValueHandling.Ignore)]
        public string Relationship { get; set; }

        [JsonProperty("user_avatar", NullValueHandling = NullValueHandling.Ignore)]
        public string UserAvatar { get; set; }

        [JsonProperty("account_type", NullValueHandling = NullValueHandling.Ignore)]
        public string AccountType { get; set; }

        [JsonProperty("venue_details", NullValueHandling = NullValueHandling.Ignore)]
        public object[] VenueDetails { get; set; }

        [JsonProperty("brewery_details", NullValueHandling = NullValueHandling.Ignore)]
        public object[] BreweryDetails { get; set; }
    }

    public partial class Welcome
    {
        public static Welcome FromJson(string json) => JsonConvert.DeserializeObject<Welcome>(json, QuickType.Converter.Settings);
    }

    public static class Serialize
    {
        public static string ToJson(this Welcome self) => JsonConvert.SerializeObject(self, QuickType.Converter.Settings);
    }

    internal static class Converter
    {
        public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
        {
            MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
            DateParseHandling = DateParseHandling.None,
            Converters = { 
                new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
            },
        };
    }
}