Swashbuckle.AspNetCore icon indicating copy to clipboard operation
Swashbuckle.AspNetCore copied to clipboard

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

Open toreamun opened this issue 1 year ago • 6 comments

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

toreamun avatar Jan 20 '23 13:01 toreamun

Hello Same for me any workaround?

7amou3 avatar Sep 02 '23 14:09 7amou3

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 avatar Nov 07 '23 18:11 TWhidden

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);
}

TWhidden avatar Nov 07 '23 20:11 TWhidden

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);
});

Pablissimo avatar Dec 01 '23 16:12 Pablissimo

This works in DotSwashbuckle, tested in 3.0.9

Havunen avatar Apr 14 '24 05:04 Havunen

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.

martincostello avatar Apr 14 '24 17:04 martincostello