domaindrivendev / Swashbuckle.AspNetCore

Swagger tools for documenting API's built on ASP.NET Core
MIT License
5.26k stars 1.31k forks source link

Exception when using System.Text.Json version 7 with System.Text.Json source generator and enums #2593

Open toreamun opened 1 year ago

toreamun commented 1 year ago

I am using Swashbuckle.AspNetCore version 6.5.0. I have created a class inherited from JsonSerializerContext to use System.Text.Json source generator, and added the context at startup:

builder.Services.AddControllers()
  .AddJsonOptions(options => { options.JsonSerializerOptions.AddContext<ApiJsonContext>(); });

The ApiJsonContext looks like this:

[JsonSerializable(typeof(WeatherForecast))]
public partial class ApiJsonContext : JsonSerializerContext
{
}

I have a controller that returns a simple object with an enum:

public class WeatherForecast
{
   public WeatherType WeatherType { get; set; }
}

public enum WeatherType
{
   Unknown = 0, 
   Bad,
   Nice
}

This works great when using System.Text.Json version 6, but breaks with this error if I upgrade to System.Text.Json version 7:

Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Failed to generate Operation for action - WebApplicationTest.Controllers.WeatherForecastController.Get (WebApplicationTest). See inner exception
  ---> Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Failed to generate schema for type - WebApplicationTest.WeatherForecast. See inner exception
  ---> System.NotSupportedException: Metadata for type 'System.Object' was not provided by TypeInfoResolver of type 'WebApplicationTest.ApiJsonContext'. 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.JsonSerializerOptions.get_ObjectTypeInfo()
    at Swashbuckle.AspNetCore.SwaggerGen.JsonSerializerDataContractResolver.GetDataContractForType(Type type)
    at Swashbuckle.AspNetCore.SwaggerGen.SchemaGenerator.GenerateSchemaForMember(Type modelType, SchemaRepository schemaRepository, MemberInfo memberInfo, DataProperty dataProperty)
    at Swashbuckle.AspNetCore.SwaggerGen.SchemaGenerator.CreateObjectSchema(DataContract dataContract, SchemaRepository schemaRepository)
    at Swashbuckle.AspNetCore.SwaggerGen.SchemaGenerator.GenerateReferencedSchema(DataContract dataContract, SchemaRepository schemaRepository, Func`1 definitionFactory)
    at Swashbuckle.AspNetCore.SwaggerGen.SchemaGenerator.GenerateConcreteSchema(DataContract dataContract, SchemaRepository schemaRepository)
    at Swashbuckle.AspNetCore.SwaggerGen.SchemaGenerator.GenerateSchemaForType(Type modelType, SchemaRepository schemaRepository)
    at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateSchema(Type type, SchemaRepository schemaRepository, PropertyInfo propertyInfo, ParameterInfo parameterInfo, ApiParameterRouteInfo routeInfo)
    --- End of inner exception stack trace ---
    at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateSchema(Type type, SchemaRepository schemaRepository, PropertyInfo propertyInfo, ParameterInfo parameterInfo, ApiParameterRouteInfo routeInfo)
    at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.CreateResponseMediaType(ModelMetadata modelMetadata, SchemaRepository schemaRespository)
    at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
    at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateResponse(ApiDescription apiDescription, SchemaRepository schemaRepository, String statusCode, ApiResponseType apiResponseType)
    at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateResponses(ApiDescription apiDescription, SchemaRepository schemaRepository)
    at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperation(ApiDescription apiDescription, SchemaRepository schemaRepository)
    --- End of inner exception stack trace ---
    at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperation(ApiDescription apiDescription, SchemaRepository schemaRepository)
    at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperations(IEnumerable`1 apiDescriptions, SchemaRepository schemaRepository)
    at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GeneratePaths(IEnumerable`1 apiDescriptions, SchemaRepository schemaRepository)
    at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwaggerDocumentWithoutFilters(String documentName, String host, String basePath)
    at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwaggerAsync(String documentName, String host, String basePath)
    at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
    at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

This error occurs only if using enum. No error if I change to string:

public class WeatherForecast
{
   public string WeatherType { get; set; }
}

Example source code

7amou3 commented 1 year ago

Hello Same for me any workaround?

TWhidden commented 1 year ago

Same issue also. Getting ready for net8, and moving to the source generation.

the type is an enum, Stack Trace shows SwaggerGen/Swashbuckle has the type:

image

I think the fault falls here though. JsonConverterFunc takes the first value of the enum, and it becomes an object (boxed?). Shouldn't it be the type passed in?

image image

I have not tried rolling this back to package release System.Text.Json 6.x yet, using 7.0.3. Have not tried the RC2 of v8 yet.

In short - using the System.Test.Json Serialization Source Gen, this will cause the throw.

image

TWhidden commented 1 year ago

As a work around -

[JsonSerializable(typeof(object))] on top of your JsonSerializerContext will get you past it.

For the fix:

On this line:

https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/8f363f7359cb1cb8fa5de5195ec6d97aefaa16b3/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs#L46C24-L46C41

Have it call a new function with the type passed in

private string JsonConverterFunc(object value, Type t, JsonSerializerOptions options)
{
    return JsonSerializer.Serialize(value, t, options);
}
Pablissimo commented 11 months ago

In case this helps anyone else, in my case for some reason the JsonSerializerDataContractResolver that was getting used by Swashbuckle to generate the swagger.json file was defaulting to a brand new JsonSerializerOptions, rather than correctly fetching the JsonSerializerOptions I'd already configured via a call to ConfigureHttpJsonOptions during startup.

The workaround for me, so far working, has been to explicitly add ISerializerDataContractResolver to the DI container before AddSwaggerGen is called so that Swashbuckle is using the same JsonSerializerContext as the rest of my application which is correctly configured with all the enums and other types I use in my API - though I'm not sure why it wasn't in the first place (possibly bad code our side):

services.AddTransient<ISerializerDataContractResolver>(sp => 
{
    var opts = sp.GetRequiredService<IOptions<JsonOptions>>().Value?.SerializerOptions 
        ?? new JsonSerializerOptions(JsonSerializerDefaults.Web);

    return new JsonSerializerDataContractResolver(opts);
});
Havunen commented 7 months ago

This works in DotSwashbuckle, tested in 3.0.9

martincostello commented 7 months ago

Thanks for the hints here in the thread.

JsonSerializerDataContractResolver seems to me to have fundamental issues with AoT compatibility with regards to be supported for all possible scenarios (or at least would need to be heavily annotated). It may however work for various simple use cases.

I've applied the suggestion from https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2593#issuecomment-1799837881 to #2800.

The reason https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2593#issuecomment-1836432544 doesn't work out of the box is because Swashbuckle hadn't been taught to understand the other JsonOptions class that is specific to Minimal APIs (as opposed to MVC). While explicit configuration is the way to go to explicitly configure which options you want to use, I've updated the code in #2799 to fall through to the options for Minimal APIs if not resolvable from MVC.

reichigo commented 4 months ago

Hello,

My workaround, as suggested by @Pablissimo, is to add the following line:

services.AddTransient<ISerializerDataContractResolver>(sp => 
{
    var opts = sp.GetRequiredService<IOptions<JsonOptions>>().Value?.SerializerOptions 
        ?? new JsonSerializerOptions(JsonSerializerDefaults.Web);

    return new JsonSerializerDataContractResolver(opts);
});

This resolves the error, but the enum example is still showing null. To solve this part, I've created the following class:

public class EnumSchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (context.Type.IsEnum)
        {
            schema.Enum.Clear();
            foreach (var name in Enum.GetNames(context.Type))
            {
                schema.Enum.Add(new OpenApiString(name));
            }
        }
    }
}

And I configured it here:

builder.Services.AddSwaggerGen(c =>
{
    c.SchemaFilter<EnumSchemaFilter>();
});
jgarciadelanoceda commented 1 month ago

Hi! In my case I just put the JsonSerializable WeatherType in the context. This isn't needed for Request/Response Bodys, but it's needed for FromPath/FromQuery/FromForm properties