How do I ignore exceptions during deserialization of bad JSON?
Asked Answered
C

5

6

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.

Coprophagous answered 31/1, 2020 at 22:56 Comment(5)
What will you like to do when exception occurs? Please note exception can occur for several other formatting error as well. If you want to ignore exception and continue serializing then one option could be use [OnError] attributes.Navarre
If I can isolate the exception to this particular thing, it would be safe to just swallow it. Maybe inside OnError() I can see if the current property is the one in question, and if so, continue?Coprophagous
You might try using JsonConvert.DeserializeObject<MyObject>("some json string", new JsonSerializerSettings { Error = MyDeserializationErrorHandler }); Newtonsoft DocumentationStonge
You can create a custom JsonConverter for your class, then do a JsonToken.StartArray check for your property and then return null from there. #40439790Lobo
@Chris I have added a solution based [OnError] attributes. Have a look.Navarre
D
6

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

    ...
}
Dingdong answered 1/2, 2020 at 2:53 Comment(5)
This is great and it appears to work. But I suppose the downside is that it doesn't support writing? I guess I'd have to do that custom anyway...Coprophagous
If I move the var rootElement = JsonDocument.ParseValue(ref reader); line into the else block where it is actually used, this solution stops working. So I moved it back. But why is that? How does passing the reader into ParseValue change the reader?Coprophagous
@Chris yeah, I didn't implement a writer, I didn't know if you'd need it and ... I was feeling lazy last night! Give me a bit and I'll come up with something for you. As for why it stops working I have no idea... I didn't even test that. Perhaps has something to do with the reader being read-only/one-way... not sureDingdong
Oh don’t feel lazy. None of this was your obligation and it works pretty well!Coprophagous
@Chris it is actually quite easy to implement a writer, just one line of code. I've updated my answer (and re-named the converter since its really converting an empty array). I initially wrote (and posted) an answer in Newtonsoft.Json but deleted it when I can up with this solution. I'm going to undelete it since it could be useful for others using Json.NetDingdong
N
4

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.

Navarre answered 1/2, 2020 at 0:42 Comment(2)
This [OnError] attribute exists in Newtonsoft library. This will not work for System.Text.Json, which is used by default in asp.net core.Pico
This is not useful for System.Text.Json, however, this was helpful for a Newtonsoft user looking for the same question with Newtonsoft.JsonAnnmaria
D
1

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.

Dingdong answered 1/2, 2020 at 0:43 Comment(1)
It's good to have this, since I might end up using Json.Net anyway. System.Text.Json doesn't have snake_case support yet.Coprophagous
T
1

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();
        }
    }
Thumbsdown answered 8/4, 2022 at 7:24 Comment(0)
C
0

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;
}
Coprophagous answered 1/2, 2020 at 0:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.