Reputation: 10627
I am consuming an API that is supposed to return an object, like
{
"some_object": {
"some_field": "some value"
}
}
when that object is null, I would expect
{
"some_object": null
}
or
{
"some_object": {}
}
But what they send me is
{
"some_object": []
}
...even though it's never an array.
When using
JsonSerializer.Deserialize<MyObject>(myJson, myOptions)
an exception is thrown when []
appears where null
is expected.
Can I selectively ignore this exception?
My current way of handling this is to read the json and fix it with a regex before deserialization.
I prefer to use System.Text.Json
, and not introduce other dependencies, if possible.
Upvotes: 6
Views: 9483
Reputation:
Solutions above work fine, I'll give mine for .NET Core 3 and above, which is just a reader, not a writer (no need). The source json, is buggy, and gives an empty array, when it should be 'null'. So, this custom converter does the correction work.
so: "myproperty":{"lahdidah": 1} is [] when it actually should be: "myproperty": null
Note, the TrySkip, we don't need to eat bogus elements.
public sealed class JsonElementOrArrayFixerConverter<T> : JsonConverter<T>
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.StartArray)
{
reader.TrySkip();
return default;
}
return JsonSerializer.Deserialize<T>(ref reader, options);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
Upvotes: 1
Reputation: 16701
Here is a solution using a custom JsonConverter and Newtonsoft.Json.
This will set SomeObject
to null in MyObject
if it is an array. You can return a new instance of SomeObject
instead by returning (T)Activator.CreateInstance(typeof(T))
.
public class ArrayToObjectConverter<T> : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
if (token.Type == JTokenType.Array)
{
// this returns null (default(SomeObject) in your case)
// if you want a new instance return (T)Activator.CreateInstance(typeof(T)) instead
return default(T);
}
return token.ToObject<T>();
}
public override bool CanConvert(Type objectType)
{
return true;
}
public override bool CanWrite
{
get { return true; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
Note that Newtonsoft.Json ignores CanConvert
(since the property is decorated with JsonConverter
attribute) it assumes it can write and convert so does not call these methods (you could return false or throw NotImplementedException instead and it will still serialize/deserialize).
In your model, decorate some_object
with the JsonConvert
attribute. Your class might look something like this:
public class MyObject
{
[JsonProperty("some_object")]
[JsonConverter(typeof(ArrayToObjectConverter<SomeObject>))]
public SomeObject SomeObject { get; set; }
}
I know you said you'd prefer to use System.Text.Json but this might be useful for others using Json.Net.
Update: I did create a JsonConverter solution using System.Text.Json and it is here.
Upvotes: 1
Reputation: 16701
This solution uses a custom JsonConverter
in System.Text.Json.
If some_object
is an array then it will return an empty object (or null if you prefer), and no exception will be thrown. Otherwise it will correctly deserialize the json.
public class EmptyArrayToObjectConverter<T> : JsonConverter<T>
{
public override T Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
{
var rootElement = JsonDocument.ParseValue(ref reader);
// if its array return new instance or null
if (reader.TokenType == JsonTokenType.EndArray)
{
// return default(T); // if you want null value instead of new instance
return (T)Activator.CreateInstance(typeof(T));
}
else
{
var text = rootElement.RootElement.GetRawText();
return JsonSerializer.Deserialize<T>(text, options);
}
}
public override bool CanConvert(Type typeToConvert)
{
return true;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize<T>(writer, value, options);
}
}
Decorate your property with the JsonConverter
attribute. Your class might look something like this:
public class MyObject
{
[JsonPropertyAttribute("some_object")]
[JsonConverter(typeof(EmptyArrayToObjectConverter<SomeObject>))]
public SomeObject SomeObject { get; set; }
...
}
Upvotes: 6
Reputation: 20095
You can use [OnError]
attribute to conditionally suppress exception related with a particular member. Let me try to explain it with an example.
The example class which represents JSON file. It contains a nested class SomeObject
.
public class MyObject
{
public int TemperatureCelsius { get; set; }
public SomeObject SomeObject { get; set; }
[OnError]
internal void OnError(StreamingContext context, ErrorContext errorContext)
{
//You can check if exception is for a specific member then ignore it
if(errorContext.Member.ToString().CompareTo("SomeObject") == 0)
{
errorContext.Handled = true;
}
}
}
public class SomeObject
{
public int High { get; set; }
public int Low { get; set; }
}
If sample JSON stream/file contains text as:
{
"TemperatureCelsius": 25,
"SomeObject": []
}
then exception is handled and suppressed as exception is raised for SomeObject
member. The SomeObject
member is set as null
.
If input JSON stream/file contains text as:
{
"TemperatureCelsius": 25,
"SomeObject":
{
"Low": 1,
"High": 1001
}
}
then object is serialized properly with SomeObject
representing expected value.
Upvotes: 4
Reputation: 9804
Exception handling is a pet peeve of mine. And I have two articles from other people that I link often on the mater:
I consider them required reading and use them as basis of any discussion on the topic.
As a general rule, Exception should never be ignored. At best they should be caught and published. At worst, they should not even be caught. It is too easy to cause followup issues and make debugging impossible to be careless or overly agressive.
That being said, in this case (deserialisation) some Exceptions could be classified as either a Exogenous or Vexing Exception. Wich are the kind you catch. And with Vexing, you might even swallow them (like TryParse() kinda does).
Usually you want to catch as specific as possible. Sometimes however you got a very wide range of Exceptions with no decent common ancestors, but shared handling. Luckily I once wrote this attempt to replicate TryParse() for someone stuck on 1.1:
//Parse throws ArgumentNull, Format and Overflow Exceptions.
//And they only have Exception as base class in common, but identical handling code (output = 0 and return false).
bool TryParse(string input, out int output){
try{
output = int.Parse(input);
}
catch (Exception ex){
if(ex is ArgumentNullException ||
ex is FormatException ||
ex is OverflowException){
//these are the exceptions I am looking for. I will do my thing.
output = 0;
return false;
}
else{
//Not the exceptions I expect. Best to just let them go on their way.
throw;
}
}
//I am pretty sure the Exception replaces the return value in exception case.
//So this one will only be returned without any Exceptions, expected or unexpected
return true;
}
Upvotes: 0