Browse Source

Adding STJ Polymorphism to Result Types (#46008)

* Adding STJ Polymorphism to Result Types

* Renaming unittest

* Adding unit tests

* Adding more unit tests

* Setting DefaultTypeInfoResolver

* Removing ISTrimmable

* Removing cache

* Clean up

* Avoiding multiple GetTypeInfo calls

* Fixing JsonResult

* Clean up

* clean up

* Adding Json apis proposal

* Removing name change

Removing the change from HttpResultsHelper to HttpResultsWriter to avoid polluting the git with not related changes and I will do it later.

* Fixing bad merge

* Fix build

* PR review

* PR Feedback

* Update for the approved API

* PR review

* Update TypedResultsTests.cs

* Changing IsPolymorphicSafe

* Fixing notnull annotation
Bruno Oliveira 3 years ago
parent
commit
0d52a44cbd

+ 3 - 3
src/Http/Http.Extensions/src/RequestDelegateFactory.cs

@@ -1055,7 +1055,7 @@ public static partial class RequestDelegateFactory
                 {
                     var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeArg);
 
-                    if (jsonTypeInfo.IsPolymorphicSafe() == true)
+                    if (jsonTypeInfo.HasKnownPolymorphism())
                     {
                         return Expression.Call(
                             ExecuteTaskOfTFastMethod.MakeGenericMethod(typeArg),
@@ -1096,7 +1096,7 @@ public static partial class RequestDelegateFactory
                 {
                     var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeArg);
 
-                    if (jsonTypeInfo.IsPolymorphicSafe() == true)
+                    if (jsonTypeInfo.HasKnownPolymorphism())
                     {
                         return Expression.Call(
                             ExecuteValueTaskOfTFastMethod.MakeGenericMethod(typeArg),
@@ -1140,7 +1140,7 @@ public static partial class RequestDelegateFactory
         {
             var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(returnType);
 
-            if (jsonTypeInfo.IsPolymorphicSafe() == true)
+            if (jsonTypeInfo.HasKnownPolymorphism())
             {
                 return Expression.Call(
                     JsonResultWriteResponseOfTFastAsyncMethod.MakeGenericMethod(returnType),

+ 27 - 23
src/Http/Http.Results/src/HttpResultsHelper.cs

@@ -1,12 +1,15 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Diagnostics.CodeAnalysis;
 using System.Text;
 using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
+using Microsoft.AspNetCore.Http.Json;
 using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
 using Microsoft.Net.Http.Headers;
 
 namespace Microsoft.AspNetCore.Http;
@@ -16,13 +19,10 @@ internal static partial class HttpResultsHelper
     internal const string DefaultContentType = "text/plain; charset=utf-8";
     private static readonly Encoding DefaultEncoding = Encoding.UTF8;
 
-    // Remove once https://github.com/dotnet/aspnetcore/pull/46008 is done.
-    [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
-    [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
-    public static Task WriteResultAsJsonAsync<T>(
+    public static Task WriteResultAsJsonAsync<TValue>(
         HttpContext httpContext,
         ILogger logger,
-        T? value,
+        TValue? value,
         string? contentType = null,
         JsonSerializerOptions? jsonSerializerOptions = null)
     {
@@ -31,32 +31,30 @@ internal static partial class HttpResultsHelper
             return Task.CompletedTask;
         }
 
-        var declaredType = typeof(T);
-        if (declaredType.IsValueType)
-        {
-            Log.WritingResultAsJson(logger, declaredType.Name);
+        jsonSerializerOptions ??= ResolveJsonOptions(httpContext).SerializerOptions;
+        var jsonTypeInfo = (JsonTypeInfo<TValue>)jsonSerializerOptions.GetTypeInfo(typeof(TValue));
 
-            // In this case the polymorphism is not
-            // relevant and we don't need to box.
+        Type? runtimeType;
+        if (jsonTypeInfo.IsValid(runtimeType = value.GetType()))
+        {
+            Log.WritingResultAsJson(logger, jsonTypeInfo.Type.Name);
             return httpContext.Response.WriteAsJsonAsync(
-                        value,
-                        options: jsonSerializerOptions,
-                        contentType: contentType);
+                value,
+                jsonTypeInfo,
+                contentType: contentType);
         }
 
-        var runtimeType = value.GetType();
-
         Log.WritingResultAsJson(logger, runtimeType.Name);
-
-        // Call WriteAsJsonAsync() with the runtime type to serialize the runtime type rather than the declared type
+        // Since we don't know the type's polymorphic characteristics
+        // our best option is use the runtime type, so,
+        // call WriteAsJsonAsync() with the runtime type to serialize the runtime type rather than the declared type
         // and avoid source generators issues.
         // https://github.com/dotnet/aspnetcore/issues/43894
         // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism
         return httpContext.Response.WriteAsJsonAsync(
-            value,
-            runtimeType,
-            options: jsonSerializerOptions,
-            contentType: contentType);
+           value,
+           jsonSerializerOptions.GetTypeInfo(runtimeType),
+           contentType: contentType);
     }
 
     public static Task WriteResultAsContentAsync(
@@ -146,6 +144,12 @@ internal static partial class HttpResultsHelper
         }
     }
 
+    private static JsonOptions ResolveJsonOptions(HttpContext httpContext)
+    {
+        // Attempt to resolve options from DI then fallback to default options
+        return httpContext.RequestServices.GetService<IOptions<JsonOptions>>()?.Value ?? new JsonOptions();
+    }
+
     internal static partial class Log
     {
         [LoggerMessage(1, LogLevel.Information,

+ 54 - 23
src/Http/Http.Results/src/JsonHttpResultOfT.cs

@@ -1,7 +1,9 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
 using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
@@ -18,33 +20,33 @@ public sealed partial class JsonHttpResult<TValue> : IResult, IStatusCodeHttpRes
     /// </summary>
     /// <param name="value">The value to format in the entity body.</param>
     /// <param name="jsonSerializerOptions">The serializer settings.</param>
-    internal JsonHttpResult(TValue? value, JsonSerializerOptions? jsonSerializerOptions)
-        : this(value, statusCode: null, contentType: null, jsonSerializerOptions: jsonSerializerOptions)
-    {
-    }
-
-    /// <summary>
-    /// Initializes a new instance of the <see cref="Json"/> class with the values.
-    /// </summary>
-    /// <param name="value">The value to format in the entity body.</param>
     /// <param name="statusCode">The HTTP status code of the response.</param>
-    /// <param name="jsonSerializerOptions">The serializer settings.</param>
-    internal JsonHttpResult(TValue? value, int? statusCode, JsonSerializerOptions? jsonSerializerOptions)
-        : this(value, statusCode: statusCode, contentType: null, jsonSerializerOptions: jsonSerializerOptions)
+    /// <param name="contentType">The value for the <c>Content-Type</c> header</param>
+    [RequiresDynamicCode(JsonHttpResultTrimmerWarning.SerializationRequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(JsonHttpResultTrimmerWarning.SerializationUnreferencedCodeMessage)]
+    internal JsonHttpResult(TValue? value, JsonSerializerOptions? jsonSerializerOptions, int? statusCode = null, string? contentType = null)
     {
+        Value = value;
+        ContentType = contentType;
+
+        if (value is ProblemDetails problemDetails)
+        {
+            ProblemDetailsDefaults.Apply(problemDetails, statusCode);
+            statusCode ??= problemDetails.Status;
+        }
+        StatusCode = statusCode;
+
+        if (jsonSerializerOptions is not null && !jsonSerializerOptions.IsReadOnly)
+        {
+            jsonSerializerOptions.TypeInfoResolver ??= new DefaultJsonTypeInfoResolver();
+        }
+
+        JsonSerializerOptions = jsonSerializerOptions;
     }
 
-    /// <summary>
-    /// Initializes a new instance of the <see cref="Json"/> class with the values.
-    /// </summary>
-    /// <param name="value">The value to format in the entity body.</param>
-    /// <param name="statusCode">The HTTP status code of the response.</param>
-    /// <param name="jsonSerializerOptions">The serializer settings.</param>
-    /// <param name="contentType">The value for the <c>Content-Type</c> header</param>
-    internal JsonHttpResult(TValue? value, int? statusCode, string? contentType, JsonSerializerOptions? jsonSerializerOptions)
+    internal JsonHttpResult(TValue? value, int? statusCode = null, string? contentType = null)
     {
         Value = value;
-        JsonSerializerOptions = jsonSerializerOptions;
         ContentType = contentType;
 
         if (value is ProblemDetails problemDetails)
@@ -59,7 +61,12 @@ public sealed partial class JsonHttpResult<TValue> : IResult, IStatusCodeHttpRes
     /// <summary>
     /// Gets or sets the serializer settings.
     /// </summary>
-    public JsonSerializerOptions? JsonSerializerOptions { get; internal init; }
+    public JsonSerializerOptions? JsonSerializerOptions { get; }
+
+    /// <summary>
+    /// Gets or sets the serializer settings.
+    /// </summary>
+    internal JsonTypeInfo? JsonTypeInfo { get; init; }
 
     /// <summary>
     /// Gets the object result.
@@ -71,7 +78,7 @@ public sealed partial class JsonHttpResult<TValue> : IResult, IStatusCodeHttpRes
     /// <summary>
     /// Gets the value for the <c>Content-Type</c> header.
     /// </summary>
-    public string? ContentType { get; internal set; }
+    public string? ContentType { get; }
 
     /// <summary>
     /// Gets the HTTP status code.
@@ -93,6 +100,30 @@ public sealed partial class JsonHttpResult<TValue> : IResult, IStatusCodeHttpRes
             httpContext.Response.StatusCode = statusCode;
         }
 
+        if (Value is null)
+        {
+            return Task.CompletedTask;
+        }
+
+        if (JsonTypeInfo != null)
+        {
+            HttpResultsHelper.Log.WritingResultAsJson(logger, JsonTypeInfo.Type.Name);
+
+            if (JsonTypeInfo is JsonTypeInfo<TValue> typedJsonTypeInfo)
+            {
+                // We don't need to box here.
+                return httpContext.Response.WriteAsJsonAsync(
+                    Value,
+                    typedJsonTypeInfo,
+                    contentType: ContentType);
+            }
+            
+            return httpContext.Response.WriteAsJsonAsync(
+                Value,
+                JsonTypeInfo,
+                contentType: ContentType);
+        }
+
         return HttpResultsHelper.WriteResultAsJsonAsync(
             httpContext,
             logger,

+ 11 - 0
src/Http/Http.Results/src/JsonHttpResultTrimmerWarning.cs

@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Http;
+
+internal class JsonHttpResultTrimmerWarning
+{
+    public const string SerializationUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext.";
+    public const string SerializationRequiresDynamicCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use the overload that takes a JsonTypeInfo or JsonSerializerContext.";
+
+}

+ 1 - 0
src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj

@@ -19,6 +19,7 @@
     <Compile Include="$(SharedSourceRoot)ProblemDetails\ProblemDetailsDefaults.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)ApiExplorerTypes\*.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)RoutingMetadata\AcceptsMetadata.cs" LinkBase="Shared" />
+    <Compile Include="$(SharedSourceRoot)Json\JsonSerializerExtensions.cs" LinkBase="Shared"/>
     <Compile Include="$(SharedSourceRoot)RouteValueDictionaryTrimmerWarning.cs" LinkBase="Shared" />
   </ItemGroup>
 

+ 7 - 1
src/Http/Http.Results/src/PublicAPI.Unshipped.txt

@@ -12,6 +12,10 @@ static Microsoft.AspNetCore.Http.Results.Created(string? uri, object? value) ->
 static Microsoft.AspNetCore.Http.Results.Created(System.Uri? uri, object? value) -> Microsoft.AspNetCore.Http.IResult!
 static Microsoft.AspNetCore.Http.Results.Created<TValue>(string? uri, TValue? value) -> Microsoft.AspNetCore.Http.IResult!
 static Microsoft.AspNetCore.Http.Results.Created<TValue>(System.Uri? uri, TValue? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Json(object? data, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Json(object? data, System.Type! type, System.Text.Json.Serialization.JsonSerializerContext! context, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Json<TValue>(TValue? data, System.Text.Json.Serialization.JsonSerializerContext! context, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Json<TValue>(TValue? data, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TValue>! jsonTypeInfo, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.IResult!
 static Microsoft.AspNetCore.Http.TypedResults.Created() -> Microsoft.AspNetCore.Http.HttpResults.Created!
 static Microsoft.AspNetCore.Http.TypedResults.Created(string? uri) -> Microsoft.AspNetCore.Http.HttpResults.Created!
 static Microsoft.AspNetCore.Http.TypedResults.Created(System.Uri? uri) -> Microsoft.AspNetCore.Http.HttpResults.Created!
@@ -22,4 +26,6 @@ static Microsoft.AspNetCore.Http.TypedResults.Created<TValue>(System.Uri? uri, T
 *REMOVED*static Microsoft.AspNetCore.Http.TypedResults.Created(System.Uri! uri) -> Microsoft.AspNetCore.Http.HttpResults.Created!
 *REMOVED*static Microsoft.AspNetCore.Http.TypedResults.Created(string! uri) -> Microsoft.AspNetCore.Http.HttpResults.Created!
 *REMOVED*static Microsoft.AspNetCore.Http.TypedResults.Created<TValue>(System.Uri! uri, TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.Created<TValue>!
-*REMOVED*static Microsoft.AspNetCore.Http.TypedResults.Created<TValue>(string! uri, TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.Created<TValue>!
+*REMOVED*static Microsoft.AspNetCore.Http.TypedResults.Created<TValue>(string! uri, TValue? value) -> Microsoft.AspNetCore.Http.HttpResults.Created<TValue>!
+static Microsoft.AspNetCore.Http.TypedResults.Json<TValue>(TValue? data, System.Text.Json.Serialization.JsonSerializerContext! context, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<TValue>!
+static Microsoft.AspNetCore.Http.TypedResults.Json<TValue>(TValue? data, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TValue>! jsonTypeInfo, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<TValue>!

+ 78 - 0
src/Http/Http.Results/src/Results.cs

@@ -6,6 +6,8 @@ using System.IO.Pipelines;
 using System.Security.Claims;
 using System.Text;
 using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Http.HttpResults;
@@ -181,9 +183,51 @@ public static partial class Results
     /// as JSON format for the response.</returns>
     /// <remarks>Callers should cache an instance of serializer settings to avoid
     /// recreating cached data with each call.</remarks>
+    [RequiresUnreferencedCode(JsonHttpResultTrimmerWarning.SerializationUnreferencedCodeMessage)]
+    [RequiresDynamicCode(JsonHttpResultTrimmerWarning.SerializationRequiresDynamicCodeMessage)]
     public static IResult Json(object? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null)
         => Json<object>(data, options, contentType, statusCode);
 
+    /// <summary>
+    /// Creates a <see cref="IResult"/> that serializes the specified <paramref name="data"/> object to JSON.
+    /// </summary>
+    /// <param name="data">The object to write as JSON.</param>
+    /// <param name="jsonTypeInfo">Metadata about the type to convert.</param>
+    /// <param name="contentType">The content-type to set on the response.</param>
+    /// <param name="statusCode">The status code to set on the response.</param>
+    /// <returns>The created <see cref="JsonHttpResult{TValue}"/> that serializes the specified <paramref name="data"/>
+    /// as JSON format for the response.</returns>
+    /// <remarks>Callers should cache an instance of serializer settings to avoid
+    /// recreating cached data with each call.</remarks>
+#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
+    public static IResult Json(object? data, JsonTypeInfo jsonTypeInfo, string? contentType = null, int? statusCode = null)
+    {
+        ArgumentNullException.ThrowIfNull(jsonTypeInfo);
+        return new JsonHttpResult<object>(data, statusCode, contentType) { JsonTypeInfo = jsonTypeInfo };
+    }
+
+    /// <summary>
+    /// Creates a <see cref="IResult"/> that serializes the specified <paramref name="data"/> object to JSON.
+    /// </summary>
+    /// <param name="data">The object to write as JSON.</param>
+    /// <param name="type">The type of object to write.</param>
+    /// <param name="context">A metadata provider for serializable types.</param>
+    /// <param name="contentType">The content-type to set on the response.</param>
+    /// <param name="statusCode">The status code to set on the response.</param>
+    /// <returns>The created <see cref="JsonHttpResult{TValue}"/> that serializes the specified <paramref name="data"/>
+    /// as JSON format for the response.</returns>
+    /// <remarks>Callers should cache an instance of serializer settings to avoid
+    /// recreating cached data with each call.</remarks>
+#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
+    public static IResult Json(object? data, Type type, JsonSerializerContext context, string? contentType = null, int? statusCode = null)
+    {
+        ArgumentNullException.ThrowIfNull(context);
+        return new JsonHttpResult<object>(data, statusCode, contentType)
+        {
+            JsonTypeInfo = context.GetRequiredTypeInfo(type)
+        };
+    }
+
     /// <summary>
     /// Creates a <see cref="IResult"/> that serializes the specified <paramref name="data"/> object to JSON.
     /// </summary>
@@ -195,11 +239,45 @@ public static partial class Results
     /// as JSON format for the response.</returns>
     /// <remarks>Callers should cache an instance of serializer settings to avoid
     /// recreating cached data with each call.</remarks>
+    [RequiresUnreferencedCode(JsonHttpResultTrimmerWarning.SerializationUnreferencedCodeMessage)]
+    [RequiresDynamicCode(JsonHttpResultTrimmerWarning.SerializationRequiresDynamicCodeMessage)]
 #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
     public static IResult Json<TValue>(TValue? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null)
 #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
         => TypedResults.Json(data, options, contentType, statusCode);
 
+    /// <summary>
+    /// Creates a <see cref="IResult"/> that serializes the specified <paramref name="data"/> object to JSON.
+    /// </summary>
+    /// <param name="data">The object to write as JSON.</param>
+    /// <param name="jsonTypeInfo">Metadata about the type to convert.</param>
+    /// <param name="contentType">The content-type to set on the response.</param>
+    /// <param name="statusCode">The status code to set on the response.</param>
+    /// <returns>The created <see cref="JsonHttpResult{TValue}"/> that serializes the specified <paramref name="data"/>
+    /// as JSON format for the response.</returns>
+    /// <remarks>Callers should cache an instance of serializer settings to avoid
+    /// recreating cached data with each call.</remarks>
+#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
+    public static IResult Json<TValue>(TValue? data, JsonTypeInfo<TValue> jsonTypeInfo, string? contentType = null, int? statusCode = null)
+#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
+        => TypedResults.Json(data, jsonTypeInfo, contentType, statusCode);
+
+    /// <summary>
+    /// Creates a <see cref="IResult"/> that serializes the specified <paramref name="data"/> object to JSON.
+    /// </summary>
+    /// <param name="data">The object to write as JSON.</param>
+    /// <param name="context">A metadata provider for serializable types.</param>
+    /// <param name="contentType">The content-type to set on the response.</param>
+    /// <param name="statusCode">The status code to set on the response.</param>
+    /// <returns>The created <see cref="JsonHttpResult{TValue}"/> that serializes the specified <paramref name="data"/>
+    /// as JSON format for the response.</returns>
+    /// <remarks>Callers should cache an instance of serializer settings to avoid
+    /// recreating cached data with each call.</remarks>
+#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
+    public static IResult Json<TValue>(TValue? data, JsonSerializerContext context, string? contentType = null, int? statusCode = null)
+#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
+        => TypedResults.Json(data, context, contentType, statusCode);
+
     /// <summary>
     /// Writes the byte-array content to the response.
     /// <para>

+ 42 - 2
src/Http/Http.Results/src/TypedResults.cs

@@ -6,6 +6,8 @@ using System.IO.Pipelines;
 using System.Security.Claims;
 using System.Text;
 using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Http.HttpResults;
@@ -191,11 +193,49 @@ public static class TypedResults
     /// <param name="statusCode">The status code to set on the response.</param>
     /// <returns>The created <see cref="JsonHttpResult{TValue}"/> that serializes the specified <paramref name="data"/>
     /// as JSON format for the response.</returns>
+    [RequiresUnreferencedCode(JsonHttpResultTrimmerWarning.SerializationUnreferencedCodeMessage)]
+    [RequiresDynamicCode(JsonHttpResultTrimmerWarning.SerializationRequiresDynamicCodeMessage)]
     public static JsonHttpResult<TValue> Json<TValue>(TValue? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null)
-        => new(data, statusCode, options)
+        => new(data, options, statusCode, contentType);
+
+    /// <summary>
+    /// Creates a <see cref="JsonHttpResult{TValue}"/> that serializes the specified <paramref name="data"/> object to JSON.
+    /// </summary>
+    /// <typeparam name="TValue">The type of object that will be JSON serialized to the response body.</typeparam>
+    /// <param name="data">The object to write as JSON.</param>
+    /// <param name="jsonTypeInfo">Metadata about the type to convert.</param>
+    /// <param name="contentType">The content-type to set on the response.</param>
+    /// <param name="statusCode">The status code to set on the response.</param>
+    /// <returns>The created <see cref="JsonHttpResult{TValue}"/> that serializes the specified <paramref name="data"/>
+    /// as JSON format for the response.</returns>
+#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
+    public static JsonHttpResult<TValue> Json<TValue>(TValue? data, JsonTypeInfo<TValue> jsonTypeInfo, string? contentType = null, int? statusCode = null)
+#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
+    {
+        ArgumentNullException.ThrowIfNull(jsonTypeInfo);
+        return new(data, statusCode, contentType) { JsonTypeInfo = jsonTypeInfo };
+    }
+
+    /// <summary>
+    /// Creates a <see cref="JsonHttpResult{TValue}"/> that serializes the specified <paramref name="data"/> object to JSON.
+    /// </summary>
+    /// <typeparam name="TValue">The type of object that will be JSON serialized to the response body.</typeparam>
+    /// <param name="data">The object to write as JSON.</param>
+    /// <param name="context">A metadata provider for serializable types.</param>
+    /// <param name="contentType">The content-type to set on the response.</param>
+    /// <param name="statusCode">The status code to set on the response.</param>
+    /// <returns>The created <see cref="JsonHttpResult{TValue}"/> that serializes the specified <paramref name="data"/>
+    /// as JSON format for the response.</returns>
+#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
+    public static JsonHttpResult<TValue> Json<TValue>(TValue? data, JsonSerializerContext context, string? contentType = null, int? statusCode = null)
+#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
+    {
+        ArgumentNullException.ThrowIfNull(context);
+        return new(data, statusCode, contentType)
         {
-            ContentType = contentType,
+            JsonTypeInfo = context.GetRequiredTypeInfo(typeof(TValue))
         };
+    }
 
     /// <summary>
     /// Writes the byte-array content to the response.

+ 109 - 26
src/Http/Http.Results/test/HttpResultsHelperTests.cs

@@ -3,6 +3,7 @@
 
 using System.Text.Json;
 using System.Text.Json.Serialization;
+using Microsoft.AspNetCore.Http.Json;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
@@ -24,16 +25,19 @@ public partial class HttpResultsHelperTests
             Name = "Write even more tests!",
         };
         var responseBodyStream = new MemoryStream();
-        var httpContext = CreateHttpContext(responseBodyStream, useJsonContext);
+        var httpContext = CreateHttpContext(responseBodyStream);
+        var serializerOptions = new JsonOptions().SerializerOptions;
+
+        if (useJsonContext)
+        {
+            serializerOptions.AddContext<TestJsonContext>();
+        }
 
         // Act
-        await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value);
+        await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions);
 
         // Assert
-        var body = JsonSerializer.Deserialize<TodoStruct>(responseBodyStream.ToArray(), new JsonSerializerOptions
-        {
-            PropertyNameCaseInsensitive = true
-        });
+        var body = JsonSerializer.Deserialize<TodoStruct>(responseBodyStream.ToArray(), serializerOptions);
 
         Assert.Equal("Write even more tests!", body!.Name);
         Assert.True(body!.IsComplete);
@@ -52,16 +56,19 @@ public partial class HttpResultsHelperTests
             Name = "Write even more tests!",
         };
         var responseBodyStream = new MemoryStream();
-        var httpContext = CreateHttpContext(responseBodyStream, useJsonContext);
+        var httpContext = CreateHttpContext(responseBodyStream);
+        var serializerOptions = new JsonOptions().SerializerOptions;
+
+        if (useJsonContext)
+        {
+            serializerOptions.AddContext<TestJsonContext>();
+        }
 
         // Act
-        await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value);
+        await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions);
 
         // Assert
-        var body = JsonSerializer.Deserialize<Todo>(responseBodyStream.ToArray(), new JsonSerializerOptions
-        {
-            PropertyNameCaseInsensitive = true
-        });
+        var body = JsonSerializer.Deserialize<Todo>(responseBodyStream.ToArray(), serializerOptions);
 
         Assert.NotNull(body);
         Assert.Equal("Write even more tests!", body!.Name);
@@ -82,16 +89,87 @@ public partial class HttpResultsHelperTests
             Child = "With type hierarchies!"
         };
         var responseBodyStream = new MemoryStream();
-        var httpContext = CreateHttpContext(responseBodyStream, useJsonContext);
+        var httpContext = CreateHttpContext(responseBodyStream);
+        var serializerOptions = new JsonOptions().SerializerOptions;
+
+        if (useJsonContext)
+        {
+            serializerOptions.AddContext<TestJsonContext>();
+        }
+
+        // Act
+        await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions);
+
+        // Assert
+        var body = JsonSerializer.Deserialize<TodoChild>(responseBodyStream.ToArray(), serializerOptions);
+
+        Assert.NotNull(body);
+        Assert.Equal("Write even more tests!", body!.Name);
+        Assert.True(body!.IsComplete);
+        Assert.Equal("With type hierarchies!", body!.Child);
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task WriteResultAsJsonAsync_Works_UsingBaseType_ForChildTypes(bool useJsonContext)
+    {
+        // Arrange
+        var value = new TodoChild()
+        {
+            Id = 1,
+            IsComplete = true,
+            Name = "Write even more tests!",
+            Child = "With type hierarchies!"
+        };
+        var responseBodyStream = new MemoryStream();
+        var httpContext = CreateHttpContext(responseBodyStream);
+        var serializerOptions = new JsonOptions().SerializerOptions;
+
+        if (useJsonContext)
+        {
+            serializerOptions.AddContext<TestJsonContext>();
+        }
 
         // Act
-        await HttpResultsHelper.WriteResultAsJsonAsync(httpContext, NullLogger.Instance, value);
+        await HttpResultsHelper.WriteResultAsJsonAsync<Todo>(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions);
 
         // Assert
-        var body = JsonSerializer.Deserialize<TodoChild>(responseBodyStream.ToArray(), new JsonSerializerOptions
+        var body = JsonSerializer.Deserialize<TodoChild>(responseBodyStream.ToArray(), serializerOptions);
+
+        Assert.NotNull(body);
+        Assert.Equal("Write even more tests!", body!.Name);
+        Assert.True(body!.IsComplete);
+        Assert.Equal("With type hierarchies!", body!.Child);
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task WriteResultAsJsonAsync_Works_UsingBaseType_ForChildTypes_WithJsonPolymorphism(bool useJsonContext)
+    {
+        // Arrange
+        var value = new TodoJsonChild()
+        {
+            Id = 1,
+            IsComplete = true,
+            Name = "Write even more tests!",
+            Child = "With type hierarchies!"
+        };
+        var responseBodyStream = new MemoryStream();
+        var httpContext = CreateHttpContext(responseBodyStream);
+        var serializerOptions = new JsonOptions().SerializerOptions;
+
+        if (useJsonContext)
         {
-            PropertyNameCaseInsensitive = true
-        });
+            serializerOptions.AddContext<TestJsonContext>();
+        }
+
+        // Act
+        await HttpResultsHelper.WriteResultAsJsonAsync<JsonTodo>(httpContext, NullLogger.Instance, value, jsonSerializerOptions: serializerOptions);
+
+        // Assert
+        var body = JsonSerializer.Deserialize<TodoJsonChild>(responseBodyStream.ToArray(), serializerOptions);
 
         Assert.NotNull(body);
         Assert.Equal("Write even more tests!", body!.Name);
@@ -99,31 +177,26 @@ public partial class HttpResultsHelperTests
         Assert.Equal("With type hierarchies!", body!.Child);
     }
 
-    private static DefaultHttpContext CreateHttpContext(Stream stream, bool useJsonContext = false)
+    private static DefaultHttpContext CreateHttpContext(Stream stream)
         => new()
         {
-            RequestServices = CreateServices(useJsonContext),
+            RequestServices = CreateServices(),
             Response =
             {
                 Body = stream,
             },
         };
 
-    private static IServiceProvider CreateServices(bool useJsonContext = false)
+    private static IServiceProvider CreateServices()
     {
         var services = new ServiceCollection();
         services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
-
-        if (useJsonContext)
-        {
-            services.ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolver = TestJsonContext.Default);
-        }
-
         return services.BuildServiceProvider();
     }
 
     [JsonSerializable(typeof(Todo))]
     [JsonSerializable(typeof(TodoChild))]
+    [JsonSerializable(typeof(JsonTodo))]
     [JsonSerializable(typeof(TodoStruct))]
     private partial class TestJsonContext : JsonSerializerContext
     { }
@@ -150,4 +223,14 @@ public partial class HttpResultsHelperTests
     {
         public string Child { get; set; }
     }
+
+    [JsonDerivedType(typeof(TodoJsonChild))]
+    private class JsonTodo : Todo
+    {
+    }
+
+    private class TodoJsonChild : JsonTodo
+    {
+        public string Child { get; set; }
+    }
 }

+ 8 - 8
src/Http/Http.Results/test/JsonResultTests.cs

@@ -77,7 +77,7 @@ public class JsonResultTests
         var jsonOptions = new JsonSerializerOptions()
         {
             WriteIndented = true,
-            DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
+            DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
         };
         var value = new Todo(10, "MyName") { Description = null };
         var result = new JsonHttpResult<object>(value, jsonSerializerOptions: jsonOptions);
@@ -203,7 +203,7 @@ public class JsonResultTests
         // Arrange
         var details = new HttpValidationProblemDetails();
 
-        var result = new JsonHttpResult<HttpValidationProblemDetails>(details, StatusCodes.Status422UnprocessableEntity, jsonSerializerOptions: null);
+        var result = new JsonHttpResult<HttpValidationProblemDetails>(details, jsonSerializerOptions: null, StatusCodes.Status422UnprocessableEntity);
         var httpContext = new DefaultHttpContext()
         {
             RequestServices = CreateServices(),
@@ -242,7 +242,7 @@ public class JsonResultTests
     public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull()
     {
         // Arrange
-        var result = new JsonHttpResult<object>(null, null);
+        var result = new JsonHttpResult<object>(null, jsonSerializerOptions: null, null, null);
         HttpContext httpContext = null;
 
         // Act & Assert
@@ -256,7 +256,7 @@ public class JsonResultTests
         var contentType = "application/json+custom";
 
         // Act & Assert
-        var result = Assert.IsAssignableFrom<IContentTypeHttpResult>(new JsonHttpResult<string>(null, StatusCodes.Status200OK, contentType, null));
+        var result = Assert.IsAssignableFrom<IContentTypeHttpResult>(new JsonHttpResult<string>(null, jsonSerializerOptions: null, StatusCodes.Status200OK, contentType));
         Assert.Equal(contentType, result.ContentType);
     }
 
@@ -267,7 +267,7 @@ public class JsonResultTests
         var contentType = "application/json+custom";
 
         // Act & Assert
-        var result = Assert.IsAssignableFrom<IStatusCodeHttpResult>(new JsonHttpResult<string>(null, StatusCodes.Status202Accepted, contentType, null));
+        var result = Assert.IsAssignableFrom<IStatusCodeHttpResult>(new JsonHttpResult<string>(null, jsonSerializerOptions: null, StatusCodes.Status202Accepted, contentType));
         Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode);
     }
 
@@ -278,7 +278,7 @@ public class JsonResultTests
         var contentType = "application/json+custom";
 
         // Act & Assert
-        var result = Assert.IsAssignableFrom<IStatusCodeHttpResult>(new JsonHttpResult<string>(null, statusCode: null, contentType, null));
+        var result = Assert.IsAssignableFrom<IStatusCodeHttpResult>(new JsonHttpResult<string>(null, jsonSerializerOptions: null, statusCode: null, contentType));
         Assert.Null(result.StatusCode);
     }
 
@@ -290,7 +290,7 @@ public class JsonResultTests
         var contentType = "application/json+custom";
 
         // Act & Assert
-        var result = Assert.IsAssignableFrom<IValueHttpResult>(new JsonHttpResult<string>(value, statusCode: null, contentType, null));
+        var result = Assert.IsAssignableFrom<IValueHttpResult>(new JsonHttpResult<string>(value, jsonSerializerOptions: null, statusCode: null, contentType));
         Assert.IsType<string>(result.Value);
         Assert.Equal(value, result.Value);
     }
@@ -303,7 +303,7 @@ public class JsonResultTests
         var contentType = "application/json+custom";
 
         // Act & Assert
-        var result = Assert.IsAssignableFrom<IValueHttpResult<string>>(new JsonHttpResult<string>(value, statusCode: null, contentType, null));
+        var result = Assert.IsAssignableFrom<IValueHttpResult<string>>(new JsonHttpResult<string>(value, jsonSerializerOptions: null, statusCode: null, contentType));
         Assert.IsType<string>(result.Value);
         Assert.Equal(value, result.Value);
     }

+ 102 - 2
src/Http/Http.Results/test/ResultsTests.cs

@@ -8,6 +8,8 @@ using System.Reflection;
 using System.Security.Claims;
 using System.Text;
 using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Routing;
@@ -15,7 +17,7 @@ using Microsoft.Net.Http.Headers;
 
 namespace Microsoft.AspNetCore.Http.HttpResults;
 
-public class ResultsTests
+public partial class ResultsTests
 {
     [Fact]
     public void Accepted_WithUrlAndValue_ResultHasCorrectValues()
@@ -780,6 +782,100 @@ public class ResultsTests
         Assert.Null(result.StatusCode);
     }
 
+    [Fact]
+    public void Json_WithTypeInfo_ResultHasCorrectValues()
+    {
+        // Act
+        var result = Results.Json(null, StringJsonContext.Default.String as JsonTypeInfo) as JsonHttpResult<object>;
+
+        // Assert
+        Assert.Null(result.Value);
+        Assert.Null(result.JsonSerializerOptions);
+        Assert.Null(result.ContentType);
+        Assert.Null(result.StatusCode);
+        Assert.Equal(StringJsonContext.Default.String, result.JsonTypeInfo);
+    }
+
+    [Fact]
+    public void Json_WithJsonContext_ResultHasCorrectValues()
+    {
+        // Act
+        var result = Results.Json(null, typeof(string), StringJsonContext.Default) as JsonHttpResult<object>;
+
+        // Assert
+        Assert.Null(result.Value);
+        Assert.Null(result.JsonSerializerOptions);
+        Assert.Null(result.ContentType);
+        Assert.Null(result.StatusCode);
+        Assert.IsAssignableFrom<JsonTypeInfo<string>>(result.JsonTypeInfo);
+    }
+
+    [Fact]
+    public void JsonOfT_WithTypeInfo_ResultHasCorrectValues()
+    {
+        // Act
+        var result = Results.Json(null, StringJsonContext.Default.String) as JsonHttpResult<string>;
+
+        // Assert
+        Assert.Null(result.Value);
+        Assert.Null(result.JsonSerializerOptions);
+        Assert.Null(result.ContentType);
+        Assert.Null(result.StatusCode);
+        Assert.Equal(StringJsonContext.Default.String, result.JsonTypeInfo);
+    }
+
+    [Fact]
+    public void JsonOfT_WithJsonContext_ResultHasCorrectValues()
+    {
+        // Act
+        var result = Results.Json<string>(null, StringJsonContext.Default) as JsonHttpResult<string>;
+
+        // Assert
+        Assert.Null(result.Value);
+        Assert.Null(result.JsonSerializerOptions);
+        Assert.Null(result.ContentType);
+        Assert.Null(result.StatusCode);
+        Assert.IsAssignableFrom<JsonTypeInfo<string>>(result.JsonTypeInfo);
+    }
+
+    [Fact]
+    public void JsonOfT_WithNullSerializerContext_ThrowsArgException()
+    {
+        Assert.Throws<ArgumentNullException>("context", () => Results.Json<object>(null, context: null));
+    }
+
+    [Fact]
+    public void Json_WithNullSerializerContext_ThrowsArgException()
+    {
+        Assert.Throws<ArgumentNullException>("context", () => Results.Json(null, type: typeof(object), context: null));
+    }
+
+    [Fact]
+    public void Json_WithInvalidSerializerContext_ThrowsInvalidOperationException()
+    {
+        var ex = Assert.Throws<InvalidOperationException>(() => Results.Json(null, type: typeof(object), context: StringJsonContext.Default));
+        Assert.Equal(ex.Message, $"Unable to obtain the JsonTypeInfo for type 'System.Object' from the context '{typeof(StringJsonContext).FullName}'.");
+    }
+
+    [Fact]
+    public void JsonOfT_WithInvalidSerializerContext_ThrowsInvalidOperationException()
+    {
+        var ex = Assert.Throws<InvalidOperationException>(() => Results.Json<object>(null, context: StringJsonContext.Default));
+        Assert.Equal(ex.Message, $"Unable to obtain the JsonTypeInfo for type 'System.Object' from the context '{typeof(StringJsonContext).FullName}'.");
+    }
+
+    [Fact]
+    public void Json_WithNullTypeInfo_ThrowsArgException()
+    {
+        Assert.Throws<ArgumentNullException>("jsonTypeInfo", () => Results.Json(null, jsonTypeInfo: null));
+    }
+
+    [Fact]
+    public void JsonOfT_WithNullTypeInfo_ThrowsArgException()
+    {
+        Assert.Throws<ArgumentNullException>("jsonTypeInfo", () => Results.Json<object>(null, jsonTypeInfo: null));
+    }
+
     [Fact]
     public void LocalRedirect_WithNullStringUrl_ThrowsArgException()
     {
@@ -1351,7 +1447,7 @@ public class ResultsTests
         (() => Results.File(Path.Join(Path.DirectorySeparatorChar.ToString(), "rooted", "path"), null, null, null, null, false), typeof(PhysicalFileHttpResult)),
         (() => Results.File("path", null, null, null, null, false), typeof(VirtualFileHttpResult)),
         (() => Results.Forbid(null, null), typeof(ForbidHttpResult)),
-        (() => Results.Json(new(), null, null, null), typeof(JsonHttpResult<object>)),
+        (() => Results.Json(new(), (JsonSerializerOptions)null, null, null), typeof(JsonHttpResult<object>)),
         (() => Results.NoContent(), typeof(NoContent)),
         (() => Results.NotFound(null), typeof(NotFound)),
         (() => Results.NotFound(new()), typeof(NotFound<object>)),
@@ -1377,4 +1473,8 @@ public class ResultsTests
     public static IEnumerable<object[]> FactoryMethodsFromTuples() => FactoryMethodsTuples.Select(t => new object[] { t.Item1, t.Item2 });
 
     private record Todo(int Id);
+
+    [JsonSerializable(typeof(string))]
+    private partial class StringJsonContext : JsonSerializerContext
+    { }
 }

+ 66 - 1
src/Http/Http.Results/test/TypedResultsTests.cs

@@ -6,6 +6,8 @@ using System.IO.Pipelines;
 using System.Security.Claims;
 using System.Text;
 using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Routing;
@@ -13,7 +15,7 @@ using Microsoft.Net.Http.Headers;
 
 namespace Microsoft.AspNetCore.Http.HttpResults;
 
-public class TypedResultsTests
+public partial class TypedResultsTests
 {
     [Fact]
     public void Accepted_WithStringUrlAndValue_ResultHasCorrectValues()
@@ -732,6 +734,65 @@ public class TypedResultsTests
         Assert.Null(result.StatusCode);
     }
 
+    [Fact]
+    public void Json_WithTypeInfo_ResultHasCorrectValues()
+    {
+        // Arrange
+        var data = default(object);
+
+        // Act
+        var result = TypedResults.Json(data, ObjectJsonContext.Default.Object);
+
+        // Assert
+        Assert.Null(result.Value);
+        Assert.Null(result.JsonSerializerOptions);
+        Assert.Null(result.ContentType);
+        Assert.Null(result.StatusCode);
+        Assert.Equal(ObjectJsonContext.Default.Object, result.JsonTypeInfo);
+    }
+
+    [Fact]
+    public void Json_WithJsonContext_ResultHasCorrectValues()
+    {
+        // Arrange
+        var data = default(object);
+
+        // Act
+        var result = TypedResults.Json(data, ObjectJsonContext.Default);
+
+        // Assert
+        Assert.Null(result.Value);
+        Assert.Null(result.JsonSerializerOptions);
+        Assert.Null(result.ContentType);
+        Assert.Null(result.StatusCode);
+        Assert.IsAssignableFrom<JsonTypeInfo<object>>(result.JsonTypeInfo);
+    }
+
+    [Fact]
+    public void Json_WithNullSerializerContext_ThrowsArgException()
+    {
+        // Arrange
+        var data = default(object);
+
+        Assert.Throws<ArgumentNullException>("context", () => TypedResults.Json(data, context: null));
+    }
+
+    [Fact]
+    public void Json_WithInvalidSerializerContext_ThrowsInvalidOperationException()
+    {
+        var ex = Assert.Throws<InvalidOperationException>(() => TypedResults.Json(string.Empty, context: ObjectJsonContext.Default));
+        Assert.Equal(ex.Message, $"Unable to obtain the JsonTypeInfo for type 'System.String' from the context '{typeof(ObjectJsonContext).FullName}'.");
+    }
+
+    [Fact]
+    public void Json_WithNullTypeInfo_ThrowsArgException()
+    {
+        // Arrange
+        var data = default(object);
+
+        Assert.Throws<ArgumentNullException>("jsonTypeInfo", () => TypedResults.Json(data, jsonTypeInfo: null));
+    }
+
     [Fact]
     public void LocalRedirect_WithNullStringUrl_ThrowsArgException()
     {
@@ -1213,4 +1274,8 @@ public class TypedResultsTests
         // Assert
         Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode);
     }
+
+    [JsonSerializable(typeof(object))]
+    private partial class ObjectJsonContext : JsonSerializerContext
+    { }
 }

+ 1 - 1
src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs

@@ -72,7 +72,7 @@ public class SystemTextJsonOutputFormatter : TextOutputFormatter
         {
             var declaredTypeJsonInfo = SerializerOptions.GetTypeInfo(context.ObjectType);
 
-            if (declaredTypeJsonInfo.IsPolymorphicSafe() || context.Object is null || runtimeType == declaredTypeJsonInfo.Type)
+            if (declaredTypeJsonInfo.IsValid(runtimeType))
             {
                 jsonTypeInfo = declaredTypeJsonInfo;
             }

+ 9 - 1
src/Shared/Json/JsonSerializerExtensions.cs

@@ -1,19 +1,27 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
 using System.Text.Json;
+using System.Text.Json.Serialization;
 using System.Text.Json.Serialization.Metadata;
 
 namespace Microsoft.AspNetCore.Http;
 
 internal static class JsonSerializerExtensions
 {
-    public static bool IsPolymorphicSafe(this JsonTypeInfo jsonTypeInfo)
+    public static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo)
      => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null;
 
+    public static bool IsValid(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType)
+     => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism();
+
     public static JsonTypeInfo GetReadOnlyTypeInfo(this JsonSerializerOptions options, Type type)
     {
         options.MakeReadOnly();
         return options.GetTypeInfo(type);
     }
+
+    public static JsonTypeInfo GetRequiredTypeInfo(this JsonSerializerContext context, Type type)
+        => context.GetTypeInfo(type) ?? throw new InvalidOperationException($"Unable to obtain the JsonTypeInfo for type '{type.FullName}' from the context '{context.GetType().FullName}'.");
 }

+ 1 - 1
src/Shared/RouteHandlers/ExecuteHandlerHelper.cs

@@ -37,7 +37,7 @@ internal static class ExecuteHandlerHelper
     {
         var runtimeType = value?.GetType();
 
-        if (runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.IsPolymorphicSafe())
+        if (jsonTypeInfo.IsValid(runtimeType))
         {
             // In this case the polymorphism is not
             // relevant for us and will be handled by STJ, if needed.