如何在反序列化 bad JSON 期间忽略异常?

How do I ignore exceptions during deserialization of bad JSON?

我正在消耗一个 API 应该 return 一个对象,比如

{
    "some_object": { 
        "some_field": "some value" 
    }
}

当该对象为空时,我希望

{
    "some_object": null
}

{
    "some_object": {}
}

但是他们发给我的是

{
    "some_object": []
}

...即使它从来都不是数组。

使用时

JsonSerializer.Deserialize<MyObject>(myJson, myOptions)

[] 出现在预期 null 的位置时抛出异常。

我可以选择性地忽略这个异常吗?

我目前的处理方式是阅读 json 并在反序列化之前用正则表达式修复它。

我更喜欢使用System.Text.Json,如果可能的话不引入其他依赖。

异常处理是我的一个小毛病。我有两篇其他人的文章,我 link 经常在这个问题上:

我认为它们是必读的,并将它们作为讨论该主题的基础。

作为一般规则,永远不应忽略异常。充其量它们应该被捕获并发布。最坏的情况下,他们甚至不应该被抓住。粗心大意或过于激进,太容易引起后续问题,使调试无法进行。

话虽这么说,在这种情况下(反序列化)一些异常可以归类为外生异常或令人烦恼的异常。你抓到的是那种。使用 Vexing,您甚至可以吞下它们(就像 TryParse() 有点那样)。

通常你希望抓得越具体越好。但是,有时您会遇到范围很广的异常,它们没有像样的共同祖先,而是共享处理。幸运的是,我曾经写过这个尝试为卡在 1.1 上的人复制 TryParse():

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

您可以使用 [OnError] 属性有条件地抑制与特定成员相关的异常。让我试着用一个例子来解释它。

代表JSON文件的例子class。它包含一个嵌套的 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; }
}

如果样本 JSON stream/file 包含文本为:

{
  "TemperatureCelsius": 25,
  "SomeObject": []
}

然后处理并抑制异常,因为 SomeObject 成员引发了异常。 SomeObject 成员设置为 null.

如果输入 JSON stream/file 包含文本为:

{
  "TemperatureCelsius": 25,
  "SomeObject":
  {
    "Low": 1,
    "High": 1001
  }
}

然后对象被正确序列化,SomeObject 代表期望值。

这是一个使用自定义 JsonConverter 和 Newtonsoft.Json 的解决方案。

如果 SomeObject 是数组,这会将 MyObject 中的 SomeObject 设置为 null。您可以 return SomeObject 的新实例,而不是 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);
    }
}

请注意 Newtonsoft.Json 忽略 CanConvert(因为 属性 装饰有 JsonConverter 属性)它假定它 可以 write 和 convert 所以不会调用这些方法(你可以 return false 或抛出 NotImplementedException 而不是它仍然会 serialize/deserialize)。

在您的模型中,使用 JsonConvert 属性修饰 some_object。您的 class 可能看起来像这样:

public class MyObject
{
    [JsonProperty("some_object")]
    [JsonConverter(typeof(ArrayToObjectConverter<SomeObject>))]
    public SomeObject SomeObject { get; set; }
}

我知道您说过您更喜欢使用 System.Text.Json,但这可能对其他使用 Json.Net 的人有用。

更新:我确实使用 System.Text.Json 创建了一个 JsonConverter 解决方案,它是

此解决方案在 System.Text.Json 中使用自定义 JsonConverter

如果 some_object 是一个数组,那么它将 return 一个空对象(如果您愿意,也可以是 null),并且 不会抛出异常 。否则它将正确反序列化 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);
    }
}

JsonConverter 属性装饰您的 属性。您的 class 可能看起来像这样:

public class MyObject
{
    [JsonPropertyAttribute("some_object")]
    [JsonConverter(typeof(EmptyArrayToObjectConverter<SomeObject>))]
    public SomeObject SomeObject { get; set; }

    ...
}

上面的解决方案工作正常,我会为 .NET Core 3 及更高版本提供我的解决方案,这只是 reader,而不是编写器(不需要)。来源 json 存在错误,并给出了一个空数组,而它应该是 'null'。因此,此自定义转换器会执行更正工作。

所以:"myproperty":{"lahdidah": 1} 是 [] 而实际上应该是:"myproperty": null

注意,TrySkip,我们不需要吃假元素。

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