【问题标题】:How do I get the property path from Utf8JsonReader?如何从 Utf8JsonReader 获取属性路径?
【发布时间】:2021-09-10 18:21:35
【问题描述】:

使用 NewtonSoft,我们可以使用reader.Path 获取路径。 System.Text.Json 没有这个。

namespace API.JsonConverters
{
    using System;
    using System.Text.Json;
    using System.Text.Json.Serialization;

    /// <summary>
    /// Use DateTime.Parse to replicate how Newtonsoft worked.
    /// </summary>
    /// <remarks>https://docs.microsoft.com/en-us/dotnet/standard/datetime/system-text-json-support</remarks>
    public class DateTimeConverterUsingDateTimeParse : JsonConverter<DateTime>
    {
        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            try
            {
                return DateTime.Parse(reader.GetString(), styles: System.Globalization.DateTimeStyles.RoundtripKind);
            }
            catch (FormatException)
            {
                // We have to do this to have the Path variable auto populated so when the middleware catches the error, it will properly populate the ModelState errors.
                // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#error-handling
                // https://github.com/dotnet/aspnetcore/blob/release/3.1/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs#L79
                throw new JsonException("Invalid DateTime. Please use RoundTripKind (MM-DD-YYYY) - https://docs.microsoft.com/en-us/dotnet/standard/base-types/how-to-round-trip-date-and-time-values");
            }
        }

        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.ToString("o"));
        }
    }
}

如何访问当前路径,以便我可以从自定义 JsonConverterRead() 方法中抛出同时包含自定义消息和 Path 的异常?

【问题讨论】:

标签: c# asp.net-mvc asp.net-core system.text.json


【解决方案1】:

JsonConverter 中,您希望使用自定义消息引发自定义异常,并包含 JSONPath 信息。正如docs 所解释的,System.Text.Json 仅将路径信息附加到 JsonException 类型的异常中 - 并且仅当异常没有消息时。那么,如何包含路径信息呢?

这样做的明显方法是从JsonConverter&lt;T&gt;.Read() 中获取当前路径并将其传递给异常的构造函数。不幸的是,System.Text.Json 不会使路径可用于Read()Write()。这可以通过检查reference source 来确认。 Utf8JsonReader 目前甚至不知道路径。 它所知道的只是使用BitStack 成员Utf8JsonReader._bitStack 的容器类型(对象或数组)堆栈,这是处理它的最低要求退出嵌套容器时状态转换正确。 JsonSerializer 确实通过具有JsonPath() 方法的ReadStack ref 结构跟踪当前堆栈。不幸的是,ReadStack 是内部的,从未暴露给应用程序或Utf8JsonReader

作为一种解决方法,您可以创建嵌套异常,其中内部异常是您所需的异常类型,外部异常是 JsonException,序列化程序会将路径填充到其中。以下是如何执行此操作的一个示例:

public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    try
    {
        return DateTime.Parse(reader.GetString(), 
                              styles: System.Globalization.DateTimeStyles.RoundtripKind); 
    }
    catch (FormatException)
    {
        var innerEx = new ProblemDetailsException("Invalid DateTime. Please use RoundTripKind (MM-DD-YYYY) - https://docs.microsoft.com/en-us/dotnet/standard/base-types/how-to-round-trip-date-and-time-values");
        throw new JsonException(null, innerEx);
    }
}

然后在更高级别上,您可以捕获JsonException 并抛出外部ProblemDetailsException,例如像这样:

public class JsonExtensions
{
    public static T Deserialize<T>(string json, JsonSerializerOptions options = default)
    {
        try
        {
            return JsonSerializer.Deserialize<T>(json, options);
        }
        catch (JsonException ex) when (ex.InnerException is ProblemDetailsException innerException)
        {
            var finalException = new ProblemDetailsException(innerException.Message, ex.Path, ex);
            throw finalException;
        }
    }
}

注意事项:

  • 这里我假设ProblemDetailsException 看起来像:

    public class ProblemDetailsException : System.Exception
    {
        public ProblemDetailsException(string message) : base(message) { }
        public ProblemDetailsException(string message, Exception inner) : base(message, inner) { }
        public ProblemDetailsException(string message, string path) : base(message) => this.Path = path;
        public ProblemDetailsException(string message, string path, Exception inner) : base(message, inner) => this.Path = path;
    
        public string Path { get; }
    }
    
  • 您可以考虑使用CultureInfo.InvariantCulture 解析您的DateTime

    return DateTime.Parse(reader.GetString(), 
                          styles: System.Globalization.DateTimeStyles.RoundtripKind, 
                          provider: System.Globalization.CultureInfo.InvariantCulture);
    

    正如目前所写,您的转换器在不同的语言环境中的功能会有所不同。或者,如果您真的想在当前语言环境中进行解析,请在代码中明确说明:

    return DateTime.Parse(reader.GetString(), 
                          styles: System.Globalization.DateTimeStyles.RoundtripKind, 
                          provider: System.Globalization.CultureInfo.CurrentCulture);
    

演示小提琴here.

【讨论】:

  • 我本质上需要转换器中的路径,因此我可以通过其中包含路径的自定义异常来代替,以便我的 API 返回正确的 ProblemDetailsException。现在,当有 400 错误请求时,ModelState 错误被序列化并返回错误属性,如$Property。当你抛出一个 JsonException 并返回它时,它会返回一个句点 $.Property
  • @VictorioBerra - 那么您可能需要在更高级别上执行此操作。在调用JsonSerializer.Deserialize() 的地方,您可以捕获JsonException 并使用捕获的异常的Path 抛出您自己的异常。路径根本没有传递到JsonConverter&lt;T&gt;.Read(),所以你不能在那里做,你甚至不能使用反射从Utf8JsonReader获取它,因为它没有存储在那里。
  • @VictorioBerra - 有关更多信息,请参阅internal sealed class JsonReaderException 的 cmets:此类存在是因为序列化程序需要捕获读取器引发的异常才能抛出具有路径信息的 JsonException i> -- 暗示读者没有路径信息。
  • 我会继续接受这一点,但我认为这里真正的问题是 System.Text.Json 缺少我们在 Netwonsoft 中拥有的许多便利功能,这是另一个。我了解您的解决方法,但是对于不应该那么困难的事情来说,它有很多样板。另外,我不明白为什么它与 ModelBinder 加载错误路径的方式不一致。 IE:为什么$.Prop VS $Prop
  • @VictorioBerra - 我同意它缺少很多东西。至于您的第二条评论,$.Prop 严格符合JSONPath syntax$Prop 不符合。在 JSONPath 语法中,$ 代表根元素,. 代表子运算符,因此$.Prop 表示 名为“Prop”的根的子元素。 $Prop 缺少子元素运算符 so 严格来说并不意味着什么,我认为这就是 MSFT 选择$.Prop 的原因。也可能是左手不知道右手在做什么的一个实例......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-21
  • 1970-01-01
  • 2011-04-04
  • 1970-01-01
  • 2021-06-24
  • 2012-03-11
  • 1970-01-01
相关资源
最近更新 更多