【问题标题】:JsonReaderException - Unexpected character encountered while parsing valueJsonReaderException - 解析值时遇到意外字符
【发布时间】:2020-05-09 16:22:46
【问题描述】:

我在 StackOverflow 上遇到了很多关于此错误的问题,但没有一个达到我正在尝试的目标。

我想要做的是将以下错误消息数组转换成更易读的内容

{
    "parent.booleanChild": [
        "Unexpected character encountered while parsing value: T. Path 'parent.booleanChild', line 0, position 0",
        "Unexpected character encountered while parsing value: r. Path 'parent.booleanChild', line 0, position 0"
    ]
}

期望的结果

{
    "parent.booleanChild": [
        "Value 'True' is not valid, only 'true', 'false' and 'null' are allowed."
    ]
}

示例请求

{
    "parent": {
        "booleanChild": True
    }
}

我已尝试实现自定义 JsonConverter,但我发现在执行转换器之前引发了 JsonReaderException

有没有人实现了类似的东西,使他们能够产生更有意义和可读的错误消息,而无需实现自定义 IInputFormatter

【问题讨论】:

  • 你的代码是什么?
  • @Crowcoder 您对哪个部分的代码感兴趣?这是一个使用 ASP.NET Core 2.2 的企业应用程序,因此无法共享确切的代码,但我可以创建一个 MCVE,尽管我怀疑默认的开箱即用 ASP.NET Core 服务也足够了。
  • 不能反序列化无效的 json。也许您应该在反序列化之前将 True 替换为 true 并同样替换为 false。
  • 这不是关于反序列化无效的 JSON,我已经明白,这是一个关于向最终用户产生更具可读性的错误,而不是使用 2 或 4 个描述相同错误的字符串的问题。跨度>
  • 您可以通过向 JsonSerializerSettings 添加错误处理程序来拦截错误。在这里,您可以获得路径 - parent.booleanChild 和 ErrorContext 中的错误消息 - 但不是值。因此,您可以将原始异常设置为“已处理”并抛出一个新异常,其中包含“仅 'true'、'false' 和 'null' 是 parent.booleanChild 的允许值”之类的文本

标签: c# asp.net-core json.net


【解决方案1】:

这是由 JsonInputFormatter ReadRequestBodyAsync 方法的行为引起的。

我遇到了一些使用自定义错误消息的选项,但没有一个是优雅的。

选项 1:覆盖 ReadRequestBody 以添加已修复的错误消息。

public class CustomJsonInputFormatter : JsonInputFormatter
{
    private readonly IArrayPool<char> charPool;
    private readonly MvcOptions options;

    public CustomJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions)
        : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
    {
        this.charPool = new JsonArrayPool<char>(charPool);
        this.options = options;
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(
        InputFormatterContext context,
        Encoding encoding)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (encoding == null)
        {
            throw new ArgumentNullException(nameof(encoding));
        }

        var request = context.HttpContext.Request;

        var suppressInputFormatterBuffering = options?.SuppressInputFormatterBuffering ?? false;

        if (!request.Body.CanSeek && !suppressInputFormatterBuffering)
        {
            // JSON.Net does synchronous reads. In order to avoid blocking on the stream, we asynchronously
            // read everything into a buffer, and then seek back to the beginning.
            request.EnableBuffering();
            Debug.Assert(request.Body.CanSeek);

            await request.Body.DrainAsync(CancellationToken.None);
            request.Body.Seek(0L, SeekOrigin.Begin);
        }

        using (var streamReader = context.ReaderFactory(request.Body, encoding))
        {
            using (var jsonReader = new JsonTextReader(streamReader))
            {
                jsonReader.ArrayPool = charPool;
                jsonReader.CloseInput = false;

                var successful = true;
                Exception exception = null;
                void ErrorHandler(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs eventArgs)
                {
                    successful = false;

                    var path = eventArgs.ErrorContext.Path;

                    var key = ModelNames.CreatePropertyModelName(context.ModelName, path);
                    context.ModelState.TryAddModelError(key, $"Invalid value specified for {path}");
                    eventArgs.ErrorContext.Handled = true;
                }

                var type = context.ModelType;
                var jsonSerializer = CreateJsonSerializer();
                jsonSerializer.Error += ErrorHandler;
                object model;
                try
                {
                    model = jsonSerializer.Deserialize(jsonReader, type);
                }
                finally
                {
                    // Clean up the error handler since CreateJsonSerializer() pools instances.
                    jsonSerializer.Error -= ErrorHandler;
                    ReleaseJsonSerializer(jsonSerializer);
                }

                if (successful)
                {
                    if (model == null && !context.TreatEmptyInputAsDefaultValue)
                    {
                        // Some nonempty inputs might deserialize as null, for example whitespace,
                        // or the JSON-encoded value "null". The upstream BodyModelBinder needs to
                        // be notified that we don't regard this as a real input so it can register
                        // a model binding error.
                        return InputFormatterResult.NoValue();
                    }
                    else
                    {
                        return InputFormatterResult.Success(model);
                    }
                }

                if (!(exception is JsonException || exception is OverflowException))
                {
                    var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception);
                    exceptionDispatchInfo.Throw();
                }

                return InputFormatterResult.Failure();
            }
        }
    }
}

选项 2:在InvalidModelStateResponseFactory 中执行模式匹配并替换错误

解析值时遇到意外字符:T. Path 'parent.booleanChild', line 0, position 0

选项 3:将 AllowInputFormatterExceptionMessages 设置为 false,并在 InvalidModelStateResponseFactory 中假设任何空白消息都是由于序列化错误造成的。

我没有将此标记为 答案,因为我相信其他人会有更好的主意。

我提出了GitHub issue,它提出了我认为可能的解决方案。

我发现的其他 SO 问题:

ASP.NET Core handling JSON deserialization problems

Overriding ModelBindingMessageProvider error messages

【讨论】:

    【解决方案2】:

    我已经实现了与我认为您在问题中提到的类似的东西,尽管它仍然很老套。而且可能非常古怪。

    老实说,我不敢相信它不受支持(我需要它的原因有很多,包括安全性和限制代码库的复杂性)——这尤其令人费解,因为新的 System.Text.Json.Serialization 还不支持许多Newtonsoft Json 提供的功能。

    我的尝试与您的Option 2 类似,尽管没有字符串模式匹配。它依赖于您能够从绑定期间引发的异常中获取合适的消息(并且确实存在异常,如果消息不是从异常中生成的,这将不起作用。)

    我在 Asp.Net Core 3 中做了这个,虽然你可能在 2.2 中做类似的事情。我选择不覆盖 JsonInputFormatter,因为我发现您还必须覆盖大部分连线才能这样做。

    在我的例子中,所有验证错误都是由特定的异常基类生成的,该基类有足够的上下文来派生应该向用户提供什么类型的响应。异常将被包装在 JsonSerializationException 中并被 JsonInputFormatter 丢弃,因为它认为这是一个运行时异常。但是,正如您指出的 JsonSerializerSettings 上的 Error 事件允许我们访问此异常。

    我将 Json.Net 提供的异常和路径保存到请求范围内的存储中,在我的 POC 案例中,我传递了通过启动时使用的服务集合获得的 IServiceProvider 引用。通过这种方式,我们可以获得对原始 HttpContext 的引用,然后 InvalidModelStateResponseFactory 可以获取该引用。

    我的启动代码如下:-

                services
                .AddControllers()
                .ConfigureApiBehaviorOptions(options => options.InvalidModelStateResponseFactory = HandleTypedErrors)
                .AddNewtonsoftJson(options => options.ApplySerializationErrorRecording(services));
        }
    
        private static IActionResult HandleTypedErrors(ActionContext actionContext)
        {
            if (actionContext.ModelState.IsValid)
            {
                return null;
            }
    
            var errors = actionContext.ModelState
                .Select(modelError => new
                {
                    State = modelError,
                    Exception = actionContext.HttpContext.RetrieveSerializationException<ErrorException>(modelError.Key)
                })
                .Where(state => state.Exception != null)
                .Select(modelError => new
                {
                    ErrorField = modelError.State.Key,
                    ErrorCode = modelError.Exception.ErrorCode,
                    ErrorDescription = modelError.Exception.Message
                })
                .ToList();
    
            return errors.Any() ? new BadRequestObjectResult(errors.ToList()) : null;
        }
    

    应用错误记录如下所示:-

    public static MvcNewtonsoftJsonOptions ApplySerializationErrorRecording(this MvcNewtonsoftJsonOptions options, IServiceCollection services)
            {
                options.SerializerSettings.Error += (sender, args) =>
                {
                    var provider = services.BuildServiceProvider();
                    var context = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
    
                    if (context == null || !(args.ErrorContext.Error is JsonException) ||
                        !(args.ErrorContext.Error.InnerException is ErrorException ex))
                        return;
    
                    var errors = context.Items["Error"] as Dictionary<string, ErrorException> ??
                                 new Dictionary<string, ErrorException>();
                    errors.Add(args.ErrorContext.Path, ex);
                    context.Items["Error"] = errors;
                };
    
                return options;
            }
    

    【讨论】:

    • 这是一种有趣的方法,我可能会创建一个技术债务项目并链接到此答案(当然,此时将其标记为已接受的答案)。您是否处理Error 事件中的序列化逻辑以及在该事件中引发异常以使其冒泡到InvalidModelStateResponseFactory 委托?我确实创建了following issue 以获得更好的支持来自定义错误响应,因为我们也有一个安全问题,即被渗透测试人员标记。
    • 在我的情况下,一个不正确的错误消息已经冒泡到InvalidModelStateFactory,因为问题发生在我们的值类型上存在的隐式转换运算符期间。但是错误状态没有异常——因为它被输入格式化程序过滤掉了,所以我需要做的就是使用模型状态键将错误消息与异常联系起来。我已经使用 ApplySerializationErrorRecording 更新了代码,以显示我的点头错误处理程序。这几乎没有经过测试,目前只是 POC。我确实觉得这些都没有必要。
    猜你喜欢
    • 2021-01-08
    • 2015-11-28
    • 2017-05-21
    • 2014-06-09
    • 2016-02-24
    • 1970-01-01
    • 1970-01-01
    • 2017-08-30
    • 1970-01-01
    相关资源
    最近更新 更多