File: Formatters\SystemTextJsonInputFormatter.cs
Web Access
Project: src\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.AspNetCore.Http;
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;
 
    /// <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);
    }
 
    /// <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;
        var (inputStream, usesTranscodingStream) = GetInputStream(httpContext, encoding);
 
        object? model;
        try
        {
            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 (usesTranscodingStream)
            {
                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 (Stream inputStream, bool usesTranscodingStream) GetInputStream(HttpContext httpContext, Encoding encoding)
    {
        if (encoding.CodePage == Encoding.UTF8.CodePage)
        {
            return (httpContext.Request.Body, false);
        }
 
        var inputStream = Encoding.CreateTranscodingStream(httpContext.Request.Body, encoding, Encoding.UTF8, leaveOpen: true);
        return (inputStream, true);
    }
 
    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);
    }
}