dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.97k stars 4.65k forks source link

System.Text.Json Source Generator System.NotSupported exception for IEnumerable Implementations #79661

Closed mphilipp622 closed 1 year ago

mphilipp622 commented 1 year ago

Description

In dotnet 7 System.Text.Json source generation, the fallback breaking change is effecting IEnumerable types that are not explicitly declared. E.G: an ASP.NET Core web api controller returns IEnumerable<MyType> and the source generator declares [JsonSerializable(typeof(IEnumerable<MyType>))]. The source generator fails when returning a List<MyType> because the List<MyType> is not explicitly declared in the source generator.

Does this mean that anywhere that I am passing a type that implements IEnumerable, I have to explicitly declare it in the source generator? If this is intentional, then it seems to make using the built-in C# interface types less flexible. I know dotnet7 introduced polymorphic serialization/deserialization. Does this mean I need to implement polymorphic serialization/deserialization of IEnumerable on my own? Is this not provided by the library out of the box?

Reproduction Steps

I created a new Console app using dotnet 7. In the top-level Program.cs file:

using System.Text.Json;
using System.Text.Json.Serialization;

string data = TestData();

Console.WriteLine(data);

string TestData()
{
    JsonSerializerOptions opt = new();
    opt.AddContext<SourceGen>();

    var data = new List<MyType>()
    {
        new MyType {ID = 1, Name = "Test1"},
        new MyType {ID = 2, Name = "Test2"},
        new MyType {ID = 3, Name = "Test3"},
    };

    return JsonSerializer.Serialize(data, opt);
}

public class MyType
{
    public int ID { get; set; }
    public string Name { get; set; }
}

[JsonSerializable(typeof(MyType))]
[JsonSerializable(typeof(IEnumerable<MyType>))]
internal partial class SourceGen : JsonSerializerContext
{
}

Expected behavior

Successful response with following payload:

[{"ID":1,"Name":"Test1"},{"ID":2,"Name":"Test2"},{"ID":3,"Name":"Test3"}]

Actual behavior

Unhandled exception. System.NotSupportedException: Metadata for type 'System.Collections.Generic.List`1[MyType]' was not provided by TypeInfoResolver of type 'SourceGen'. If using source generation, ensure that all root types passed to the serializer have been indicated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically.
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException_NoMetadataForType(Type type, IJsonTypeInfoResolver resolver)
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Boolean resolveIfMutable)
   at System.Text.Json.JsonSerializer.GetTypeInfo(JsonSerializerOptions options, Type inputType)
   at System.Text.Json.JsonSerializer.GetTypeInfo[T](JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options)
   at Program.<<Main>$>g__TestData|0_0() in C:\Users\mphil\Documents\Projects\TestJsonError\Program.cs:line 20
   at Program.<Main>$(String[] args) in C:\Users\mphil\Documents\Projects\TestJsonError\Program.cs:line 4

Regression?

This worked in dotnet 6. Dotnet 7 introduced the breaking change for the source generator fallback, as specified here.

Known Workarounds

As per the breaking change article, I understand you can re-enable the reflection-based fallback globally. However, I still think having a solution for commonly used built-in interfaces, like IEnumerable, would be beneficial.

Configuration

.NET Version: 7.0 OS: Windows 11 Architecture: x64 Specific to configuration? No. This has also occurred on my AWS Linux 2022 arm64 box. Blazor browsers: Not applicable to this reproduction.

Other information

No response

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis See info in area-owners.md if you want to be subscribed.

Issue Details
### Description In dotnet 7 System.Text.Json source generation, the fallback breaking change is effecting `IEnumerable` types that are not explicitly declared. E.G: an ASP.NET Core web api controller returns `IEnumerable` and the source generator declares `[JsonSerializable(typeof(IEnumerable))]`. The source generator fails when returning a `List` because the `List` is not explicitly declared in the source generator. Does this mean that anywhere that I am passing a type that implements `IEnumerable`, I have to explicitly declare it in the source generator? If this is intentional, then it seems to make using the built-in C# interface types less flexible. I know dotnet7 introduced polymorphic serialization/deserialization. Does this mean I need to implement polymorphic serialization/deserialization of IEnumerable on my own? Is this not provided by the library out of the box? ### Reproduction Steps I am doing this reproduction in an ASP.NET Core web API. So the first step is to make a basic ASP.NET Core web API project and create the following resources. MyType.cs: ```csharp public class MyType { public int ID { get; set; } public string Name { get; set; } } ``` TestController.cs: ```csharp using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; [ApiController] [Route("Portal/api/[controller]")] public class TestController : ControllerBase { [HttpGet("TestEndpoint")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetMyType(CancellationToken abortToken) { List data = new() { new MyType {ID = 1, Name = "Test1"}, new MyType {ID = 2, Name = "Test2"}, new MyType {ID = 3, Name = "Test3"}, }; return await Task.FromResult(Ok(data)); } } ``` SourceGenerator.cs: ```csharp [JsonSerializable(typeof(MyType))] [JsonSerializable(typeof(IEnumerable))] internal partial class ServerSourceGenerator : JsonSerializerContext { } ``` Adding source generator to Program.cs: ```csharp services.AddControllersWithViews(opt => .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; // Add source generators options.JsonSerializerOptions.AddContext(); }) ``` ### Expected behavior Successful GET response with following payload: ```csharp [ { "ID": 1, "Name": "Test1" }, { "ID": 2, "Name": "Test2" }, { "ID": 3, "Name": "Test3" } ] ``` ### Actual behavior When running the server and sending the GET request, I receive this: ``` System.NotSupportedException: Metadata for type 'System.Collections.Generic.List`1[MyType]' was not provided by TypeInfoResolver of type 'ServerSourceGenerator'. If using source generation, ensure that all root types passed to the serializer have been indicated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically. at System.Text.Json.ThrowHelper.ThrowNotSupportedException_NoMetadataForType(Type type, IJsonTypeInfoResolver resolver) at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Boolean resolveIfMutable) at System.Text.Json.JsonSerializer.GetTypeInfo(JsonSerializerOptions options, Type inputType) at System.Text.Json.JsonSerializer.SerializeAsync(Stream utf8Json, Object value, Type inputType, JsonSerializerOptions options, CancellationToken cancellationToken) at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|30_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Program.<>c.<<
$>b__0_15>d.MoveNext() in C:\Users\mphil\Documents\Projects\ucrbraingamecenter.org\BGCPortalWASM\BGCPortalWASM\Server\Program.cs:line 295 --- End of stack trace from previous location --- at Program.<>c.<<
$>b__0_15>d.MoveNext() in C:\Users\mphil\Documents\Projects\ucrbraingamecenter.org\BGCPortalWASM\BGCPortalWASM\Server\Program.cs:line 303 --- End of stack trace from previous location --- at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext) at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext) at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) ``` ### Regression? This worked in dotnet 6. Dotnet 7 introduced the breaking change for the source generator fallback, as specified [here](https://learn.microsoft.com/en-us/dotnet/core/compatibility/serialization/7.0/reflection-fallback). ### Known Workarounds As per [the breaking change article](https://learn.microsoft.com/en-us/dotnet/core/compatibility/serialization/7.0/reflection-fallback#recommended-action), I understand you can re-enable the reflection-based fallback globally. However, I still think having a solution for commonly used built-in interfaces, like `IEnumerable`, would be beneficial. ### Configuration .NET Version: 7.0 OS: Windows 11 Architecture: x64 Specific to configuration? No. This has also occurred on my AWS Linux 2022 arm64 box. Blazor browsers: Not applicable to this reproduction. ### Other information _No response_
Author: mphilipp622
Assignees: -
Labels: `area-System.Text.Json`
Milestone: -
eiriktsarpalis commented 1 year ago

This is by design -- any type passed to the source generator (including generic instantiations) must be specified explicitly via the JsonSerializable attribute. This is because generating metadata for generic instantiations or types deriving from what has been declared always requires reflection that is not supported in trimmed/NativeAOT applications, which is the key use case for source generators. If your code doesn't use trimming or NativeAOT, you can avoid the need to explicitly declare every type by using the reflection-based serializer.