Skip to content

Stop Calling JSON GetTypeInfo with the runtime type #47548

Closed
@eerhardt

Description

@eerhardt

In both the RDF and RDG we have code like the following:

public static Task WriteJsonResponseAsync<T>(HttpResponse response, T? value, JsonSerializerOptions options, JsonTypeInfo<T> jsonTypeInfo)
{
var runtimeType = value?.GetType();
if (jsonTypeInfo.IsValid(runtimeType))
{
// In this case the polymorphism is not
// relevant for us and will be handled by STJ, if needed.
return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value!, jsonTypeInfo, default);
}
// 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
var runtimeTypeInfo = options.GetTypeInfo(runtimeType);
return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value!, runtimeTypeInfo, default);
}

This doesn't work with the unspeakable types behavior that was added to System.Text.Json - dotnet/runtime#83631. In particular, GetTypeInfo(runtimeType) doesn't work when using unspeakable types.

We started using the "runtimeType" back in .NET Core 3.0 (bed3542) when we switched from Json.Net to System.Text.Json and wanted to maintain the polymorphic serialization behavior of Json.Net (ex. if the "declared type" was a base type, and the endpoint returned a derived type, we want to serialize all the derived properties). This strategy was adopted by Minimal APIs as well, when they were introduced.

This approach was then changed early in .NET 8 in order to support "System.Text.Json Polymorphism" features that were added in .NET 7. (See #45405). The strategy used by that PR was: "when a JsonPolymorphismOptions is detected, uses the declared type's JsonTypeInfo to call the serializer." When it isn't detected, use the "runtimeType"'s JsonTypeInfo to maintain the above polymorphic serialization behavior to not break backwards compatibility.

In order to support all these scenarios, we need to tweak our JSON serialization strategy. After consulting with @brunolins16 and @eiriktsarpalis, the strategy we have settled on is:

  1. When a JsonPolymorphismOptions is detected (or when polymorphism is not possible - ex. ValueType, sealed, etc), use the declared type's JsonTypeInfo to call the serializer.
    • This is no change to what we do today in .NET 8.
  2. When the above rule doesn't work, instead of using the "runtimeType", we will serialize the value "as object" - i.e. JsonSerializer.Serialize<object>(value, options).

A drawback to this strategy is that JsonSerializer.Serialize<object>(value, options) is not guaranteed to be trim/AOT-compatible, and thus it is annotated as incompatible. We will need to suppress these warnings in ASP.NET, which is OK because of the other mechanisms we are adding to support trimming/AOT - dotnet/runtime#83279.

However, we can't do this until the JSON Trimming feature switch is merged, or we will regress the AOT size and warnings. We may also need to wait for System.Text.Json Polymorphic Type Resolving Issue.

Metadata

Metadata

Assignees

Labels

area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcfeature-rdffeature-rdg

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions