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.