dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.44k stars 10.03k forks source link

[9.0-preview.5] Confusing need to include parameters in custom JSON serializer for native AoT support with OpenAPI #56021

Open martincostello opened 5 months ago

martincostello commented 5 months ago

Is there an existing issue for this?

Describe the bug

I was playing around with the native AoT support for the new OpenAPI feature in the .NET 9 preview.5 nightly builds (9.0.100-preview.5.24281.15 specifically), and on rendering the document when debugging I got the following error:

NotSupportedException: JsonTypeInfo metadata for type 'System.Nullable`1[System.Boolean]' was not provided by TypeInfoResolver of type '[Microsoft.AspNetCore.OpenApi.OpenApiJsonSchemaContext, MartinCostello.Api.ApplicationJsonSerializerContext]'. If using source generation, ensure that all root types passed to the serializer have been annotated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically.

System.Text.Json.ThrowHelper.ThrowNotSupportedException_NoMetadataForType(Type type, IJsonTypeInfoResolver resolver)
System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, bool ensureConfigured, Nullable<bool> ensureNotNull, bool resolveIfMutable, bool fallBackToNearestAncestorType)
System.Text.Json.JsonSerializerOptions.GetTypeInfo(Type type)
JsonSchemaMapper.JsonSchemaMapper.GetJsonSchema(JsonSerializerOptions options, ParameterInfo parameterInfo, JsonSchemaMapperConfiguration configuration)
Microsoft.AspNetCore.OpenApi.OpenApiComponentService.CreateSchema(ValueTuple<Type, ParameterInfo> key)
System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue>.GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
Microsoft.AspNetCore.OpenApi.OpenApiComponentService.GetOrCreateSchema(Type type, ApiParameterDescription parameterDescription)
Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetParameters(ApiDescription description)
Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOperation(ApiDescription description, HashSet<OpenApiTag> capturedTags)
Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOperations(IGrouping<string, ApiDescription> descriptions, HashSet<OpenApiTag> capturedTags)
Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiPaths(HashSet<OpenApiTag> capturedTags)
Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiDocumentAsync(CancellationToken cancellationToken)
Microsoft.AspNetCore.Builder.OpenApiEndpointRouteBuilderExtensions+<>c__DisplayClass0_0+<<MapOpenApi>b__0>d.MoveNext()
Microsoft.AspNetCore.Http.Generated.<GeneratedRouteBuilderExtensions_g>F56B68D2B55B5B7B373BA2E4796D897848BC0F04A969B1AF6260183E8B9E0BAF2__GeneratedRouteBuilderExtensionsCore+<>c__DisplayClass2_0+<<MapGet0>g__RequestHandler|5>d.MoveNext()
Microsoft.AspNetCore.ResponseCompression.ResponseCompressionMiddleware.InvokeCore(HttpContext context)
Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
MartinCostello.Api.Middleware.CustomHttpHeadersMiddleware.Invoke(HttpContext context) in CustomHttpHeadersMiddleware.cs

None of my API models use bool?, so I was a bit confused as to what was causing the problem.

Doing a find in Visual Studio for bool? lead me to an optional query string parameter on one of my endpoints: code

I'm not sure what, if anything, can be done here, but the local developer experience digging through as to what needs to be added to my custom JsonSerializerContext (in this case adding [JsonSerializable(typeof(bool?))]) for things to render isn't great, and I imagine could be quite frustrating in a larger application.

It's also a bit confusing at first, as when I think of custom JSON serialization for AoT I think of custom types, not built-in primitive parameters. It's not needed for Request Delegate Generator to work for the actual API endpoints, for example.

Expected Behavior

Either:

Steps To Reproduce

Exceptions (if any)

NotSupportedException: JsonTypeInfo metadata for type 'System.Nullable`1[System.Boolean]' was not provided by TypeInfoResolver of type '[Microsoft.AspNetCore.OpenApi.OpenApiJsonSchemaContext, MartinCostello.Api.ApplicationJsonSerializerContext]'. If using source generation, ensure that all root types passed to the serializer have been annotated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically.

System.Text.Json.ThrowHelper.ThrowNotSupportedException_NoMetadataForType(Type type, IJsonTypeInfoResolver resolver)
System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, bool ensureConfigured, Nullable<bool> ensureNotNull, bool resolveIfMutable, bool fallBackToNearestAncestorType)
System.Text.Json.JsonSerializerOptions.GetTypeInfo(Type type)
JsonSchemaMapper.JsonSchemaMapper.GetJsonSchema(JsonSerializerOptions options, ParameterInfo parameterInfo, JsonSchemaMapperConfiguration configuration)
Microsoft.AspNetCore.OpenApi.OpenApiComponentService.CreateSchema(ValueTuple<Type, ParameterInfo> key)
System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue>.GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
Microsoft.AspNetCore.OpenApi.OpenApiComponentService.GetOrCreateSchema(Type type, ApiParameterDescription parameterDescription)
Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetParameters(ApiDescription description)
Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOperation(ApiDescription description, HashSet<OpenApiTag> capturedTags)
Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOperations(IGrouping<string, ApiDescription> descriptions, HashSet<OpenApiTag> capturedTags)
Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiPaths(HashSet<OpenApiTag> capturedTags)
Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiDocumentAsync(CancellationToken cancellationToken)
Microsoft.AspNetCore.Builder.OpenApiEndpointRouteBuilderExtensions+<>c__DisplayClass0_0+<<MapOpenApi>b__0>d.MoveNext()
Microsoft.AspNetCore.Http.Generated.<GeneratedRouteBuilderExtensions_g>F56B68D2B55B5B7B373BA2E4796D897848BC0F04A969B1AF6260183E8B9E0BAF2__GeneratedRouteBuilderExtensionsCore+<>c__DisplayClass2_0+<<MapGet0>g__RequestHandler|5>d.MoveNext()
Microsoft.AspNetCore.ResponseCompression.ResponseCompressionMiddleware.InvokeCore(HttpContext context)
Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
MartinCostello.Api.Middleware.CustomHttpHeadersMiddleware.Invoke(HttpContext context) in CustomHttpHeadersMiddleware.cs

.NET Version

9.0.100-preview.5.24281.15

Anything else?

No response

captainsafia commented 5 months ago

@martincostello Thanks for filling this issue!

I think this ties into a larger conversation around the friction that's involved when properly moving to Native AoT with System.Text.Json.

We had some discussions around solving this last year and I did some explorations into the second bullet you mentioned here:

an analyser to help identify the things you need to add to your serialization context?

Where we would ship an analyzer out-of-the-box in the shared framework that would provide codefixers that would annotate your JsonSerializerContext properly with attributes for the types exposed by your endpoints. The prototyping was promising but we haven't pursued formalizing this into an official framework feature at the moment.

This issue is popping up in more places though so it might be worth making a concerted effort to ship something here.

I'll stick this issue in the backlog for now. I'm interested in solving this problem but I think I'll have to finish building my time machine before I get around to that. šŸ˜…

martincostello commented 5 months ago

Sure, that's fine šŸ‘ - it was mainly notable/confusing for me as I've not had to annotate a type of an argument before, typically it's just been the types for things I'm explicitly serializing myself or for values returned from methods.

captainsafia commented 5 months ago

@martincostello Ah, good note.

For posterity (and others reading this thread), the big difference in the OpenAPI scenario is that we're leaning into System.Text.Json to resolve JSON schemas for all arguments. Prior to this, System.Text.Json was only coming into play for the (de)serialization of complex types in minimal APIs. Simple types were bound independently by the generated request delegate generated code. That's not longer the case.

This is indeed surprising behavior if you're not familiar with the details of the implementation and provides a bit more ammo for pursuing the codefixer approach that I mentioned earlier for these kinds of things.