Christopher
Christopher

Reputation: 10627

How do I ignore exceptions during deserialization of bad JSON?

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

Answers (5)

user2410689
user2410689

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

haldo
haldo

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

haldo
haldo

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

MKR
MKR

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

Christopher
Christopher

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

Related Questions