Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop Calling JSON GetTypeInfo with the runtime type #47548

Closed
eerhardt opened this issue Apr 3, 2023 · 6 comments · Fixed by #47859
Closed

Stop Calling JSON GetTypeInfo with the runtime type #47548

eerhardt opened this issue Apr 3, 2023 · 6 comments · Fixed by #47859
Assignees
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-rdf feature-rdg
Milestone

Comments

@eerhardt
Copy link
Member

eerhardt commented Apr 3, 2023

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.

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label Apr 3, 2023
@eerhardt eerhardt added area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels and removed needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically labels Apr 3, 2023
@brunolins16
Copy link
Member

If the polymorphism behavior is now cover inside STJ and the call to the GetTypeInfo is not needed anymore, should this also be removed?

TypeInfoResolver = TrimmingAppContextSwitches.EnsureJsonTrimmability ? null : CreateDefaultTypeResolver()

@eerhardt
Copy link
Member Author

eerhardt commented Apr 3, 2023

If the polymorphism behavior is now cover inside STJ and the call to the GetTypeInfo is not needed anymore, should this also be removed?

We will still have other calls to GetTypeInfo, which I believe are still needed. For example:

var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(returnType);

@captainsafia captainsafia self-assigned this Apr 10, 2023
@captainsafia
Copy link
Member

I can take a look at this and also make sure that we emit the same invocations between RDF and RDG. Right now they are functionally the same, but syntactically different.

@captainsafia captainsafia added feature-rdg area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-rdf and removed area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Apr 10, 2023
@eerhardt
Copy link
Member Author

@captainsafia - I can take this. I have a handful (3 maybe?) JSON changes in the works:

  1. react to Add a JsonSerializerOptions.TryGetTypeInfo method. runtime#84411 (comment)
  2. Remove our feature switch and instead use Implement the JsonSerializer.IsReflectionEnabledByDefault feature switch runtime#83844 (comment)
  3. This change.

@eerhardt eerhardt assigned eerhardt and unassigned captainsafia Apr 11, 2023
@captainsafia
Copy link
Member

@eerhardt Awesome! Is it OK to put this in the preview4 milestone?

@eerhardt eerhardt added this to the 8.0-preview4 milestone Apr 11, 2023
@eerhardt
Copy link
Member Author

Done.

eerhardt added a commit to eerhardt/aspnetcore that referenced this issue Apr 25, 2023
Calling GetTypeInfo with the object's runtime type doesn't work when using unspeakable types and using JSON source generation. There is no way to mark the unspeakable type (like a C# compiler implemented IAsyncEnumerable iterator) as JsonSerializable.

Instead, when the "declared type"'s JsonTypeInfo isn't compatible with the runtime type w.r.t. polymorphism, serialize the value "as object", letting System.Text.Json's serialization take over to serialize the value.

Fix dotnet#47548
eerhardt added a commit that referenced this issue Apr 25, 2023
* Stop Calling JSON GetTypeInfo with the runtime type

Calling GetTypeInfo with the object's runtime type doesn't work when using unspeakable types and using JSON source generation. There is no way to mark the unspeakable type (like a C# compiler implemented IAsyncEnumerable iterator) as JsonSerializable.

Instead, when the "declared type"'s JsonTypeInfo isn't compatible with the runtime type w.r.t. polymorphism, serialize the value "as object", letting System.Text.Json's serialization take over to serialize the value.

Fix #47548

* Move RequestDelegateFactory tests to be shared with RDG

* Add justifications for suppressing the warnings from JsonSerializer.

* Convert RequestDelegateWritesAsJsonResponseBody_WithJsonSerializerContext to shared test between RDF and RDG.

* Remove redundant JsonSerializerOptions.

* Rename JsonTypeInfo extension method IsValid to ShouldUseWith.
eerhardt added a commit to eerhardt/Benchmarks that referenced this issue Apr 28, 2023
dotnet/aspnetcore#47548 has now been fixed. Removing the workaround that was added for it.
eerhardt added a commit to aspnet/Benchmarks that referenced this issue Apr 28, 2023
dotnet/aspnetcore#47548 has now been fixed. Removing the workaround that was added for it.
@ghost ghost locked as resolved and limited conversation to collaborators May 25, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-rdf feature-rdg
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants
@captainsafia @eerhardt @brunolins16 and others