| File: Formatters\SystemTextJsonInputFormatter.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.Formatters; /// <summary> /// A <see cref="TextInputFormatter"/> for JSON content that uses <see cref="JsonSerializer"/>. /// </summary> public partial class SystemTextJsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy { private readonly JsonOptions _jsonOptions; private readonly ILogger<SystemTextJsonInputFormatter> _logger; private readonly bool _useStreamJsonOverload; /// <summary> /// Initializes a new instance of <see cref="SystemTextJsonInputFormatter"/>. /// </summary> /// <param name="options">The <see cref="JsonOptions"/>.</param> /// <param name="logger">The <see cref="ILogger"/>.</param> public SystemTextJsonInputFormatter( JsonOptions options, ILogger<SystemTextJsonInputFormatter> logger) { SerializerOptions = options.JsonSerializerOptions; _jsonOptions = options; _logger = logger; SupportedEncodings.Add(UTF8EncodingWithoutBOM); SupportedEncodings.Add(UTF16EncodingLittleEndian); SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson); SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson); SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax); // Fallback to the stream-based overloads for JsonSerializer.DeserializeAsync // This is to give users with custom JsonConverter implementations the chance to update their // converters to support ReadOnlySequence<T> if needed while still keeping their apps working. _useStreamJsonOverload = AppContext.TryGetSwitch("Microsoft.AspNetCore.UseStreamBasedJsonParsing", out var isEnabled) && isEnabled; } /// <summary> /// Gets the <see cref="JsonSerializerOptions"/> used to configure the <see cref="JsonSerializer"/>. /// </summary> /// <remarks> /// A single instance of <see cref="SystemTextJsonInputFormatter"/> is used for all JSON formatting. Any /// changes to the options will affect all input formatting. /// </remarks> public JsonSerializerOptions SerializerOptions { get; } /// <inheritdoc /> InputFormatterExceptionPolicy IInputFormatterExceptionPolicy.ExceptionPolicy => InputFormatterExceptionPolicy.MalformedInputExceptions; /// <inheritdoc /> public sealed override async Task<InputFormatterResult> ReadRequestBodyAsync( InputFormatterContext context, Encoding encoding) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(encoding); var httpContext = context.HttpContext; object? model; Stream? inputStream = null; try { if (encoding.CodePage == Encoding.UTF8.CodePage) { if (_useStreamJsonOverload) { model = await JsonSerializer.DeserializeAsync(httpContext.Request.Body, context.ModelType, SerializerOptions); } else { model = await JsonSerializer.DeserializeAsync(httpContext.Request.BodyReader, context.ModelType, SerializerOptions); } } else { inputStream = Encoding.CreateTranscodingStream(httpContext.Request.Body, encoding, Encoding.UTF8, leaveOpen: true); model = await JsonSerializer.DeserializeAsync(inputStream, context.ModelType, SerializerOptions); } } catch (JsonException jsonException) { var path = jsonException.Path ?? string.Empty; var modelStateException = WrapExceptionForModelState(jsonException); context.ModelState.TryAddModelError(path, modelStateException, context.Metadata); Log.JsonInputException(_logger, jsonException); return InputFormatterResult.Failure(); } catch (Exception exception) when (exception is FormatException || exception is OverflowException) { // The code in System.Text.Json never throws these exceptions. However a custom converter could produce these errors for instance when // parsing a value. These error messages are considered safe to report to users using ModelState. context.ModelState.TryAddModelError(string.Empty, exception, context.Metadata); Log.JsonInputException(_logger, exception); return InputFormatterResult.Failure(); } finally { if (inputStream is not null) { await inputStream.DisposeAsync(); } } 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 { Log.JsonInputSuccess(_logger, context.ModelType); return InputFormatterResult.Success(model); } } private Exception WrapExceptionForModelState(JsonException jsonException) { if (!_jsonOptions.AllowInputFormatterExceptionMessages) { // This app is not opted-in to System.Text.Json messages, return the original exception. return jsonException; } // InputFormatterException specifies that the message is safe to return to a client, it will // be added to model state. return new InputFormatterException(jsonException.Message, jsonException); } private static partial class Log { [LoggerMessage(1, LogLevel.Debug, "JSON input formatter threw an exception: {Message}", EventName = "SystemTextJsonInputException")] private static partial void JsonInputException(ILogger logger, string message); public static void JsonInputException(ILogger logger, Exception exception) => JsonInputException(logger, exception.Message); [LoggerMessage(2, LogLevel.Debug, "JSON input formatter succeeded, deserializing to type '{TypeName}'", EventName = "SystemTextJsonInputSuccess")] private static partial void JsonInputSuccess(ILogger logger, string? typeName); public static void JsonInputSuccess(ILogger logger, Type modelType) => JsonInputSuccess(logger, modelType.FullName); } }