I am attempting to create a null safe json converter that handles json string values like "null" and return a default instance of a type.
So far it's been working as expected, but I'm running into a casting issue with nullable integers in a nested object on the 'id' property
{
"company_detail": null,
"country_code": [
{
"id": 1,
"code": "1",
"country_name": "United States (+1)"
},
{
"id": 2,
"code": "1",
"country_name": "Canada (+1)"
},
{
"id": 3,
"code": "44",
"country_name": "United Kingdom (+44)"
}
]
}
The exception is thrown when reading a session variable from HttpContext or when deserializing manually
return _accessor.HttpContext.Session.Get<UserInformation>(SessionKeyNames.UserInformation);
var info = JsonConvert.DeserializeObject<UserInformation>(JsonConvert.SerializeObject(new UserInformation
{
CountryCode = new List<CountryCode> { new CountryCode { Id = 1, Code = "1", CountryName = "United States"} }
}));
Newtonsoft.Json.JsonSerializationException: 'Error setting value to 'Id' on 'Models.CountryCode'.'
InvalidCastException: Unable to cast object of type 'System.Int64' to type 'System.Nullable`1[System.Int32]'.
public class NullSafeJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => true;
public override bool CanRead => true;
public override bool CanWrite => false;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
var contract = serializer.ContractResolver.ResolveContract(objectType);
bool asString = objectType == typeof(string);
object obj = asString ? String.Empty : obj = contract.DefaultCreator();
switch (reader.TokenType)
{
case JsonToken.Null:
case JsonToken.None:
break;
case JsonToken.StartArray:
var arr = JArray.Load(reader);
serializer.Populate(arr.CreateReader(), obj);
break;
case JsonToken.String:
var nullish = String.IsNullOrEmpty((string)reader.Value) || Regex.IsMatch((string)reader.Value, @"^['""]?null['""]?$");
obj = (nullish && !asString) ? contract.DefaultCreator() : reader.Value;
break;
case JsonToken.StartObject:
var o = JObject.Load(reader);
//obj = o.ToObject(objectType);
serializer.Populate(o.CreateReader(), obj);
break;
default:
obj = reader.Value;
break;
}
return obj;
}
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
The type that is throwing the exception is the 'CountryCode' property
public class UserInformation
{
/// <summary>
/// Company detail.
/// </summary>
public CompanyDetail CompanyDetail { get; set; }
/// <summary>
/// country code list
/// </summary>
public List<CountryCode> CountryCode { get; set; }
}
public class CountryCode
{
[JsonProperty(PropertyName = "id")]
public int? Id
{
get;
set;
}
[JsonProperty(PropertyName = "code")]
public string Code
{
get;
set;
}
[JsonProperty(PropertyName = "country_name")]
public string CountryName
{
get;
set;
}
public CountryCode()
{
}
public CountryCode(int? id = null, string code = null, string countryName = null)
{
Id = id;
Code = code;
CountryName = countryName;
}
}
My startup file contains
public override void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
JsonConvert.DefaultSettings().Converters.Add(new NullSafeJsonConverter());
}
Note that, for nullable value types, the converter is probably not necessary.
Your immediate problem is here:
You have no guarantee that the value in the reader is of the same type as the required
objectType.JsonTextReaderparses integers aslongand floating-point values asdouble, butobjectTypemight beint,decimal, or even some enum type. So you will need to deserialize or convertreader.Valueto the required type.The following version of the converter corrects this problem by calling
Convert.ChangeType()for primitive types, and fixes a few other issues as well:That being said, I see a few problems with this approach:
DateTime.DataTableandKeyValuePair<TKey, TValue>.null(and"null") values to non-null defaults, but if a value is missing completely the corresponding property value may still benull.However the specific problem in the question is resolved.
Demo fiddle here.