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

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

Open
toreamun opened this issue Jan 20, 2023 · 8 comments
Labels
help-wanted A change up for grabs for contributions from the community

Comments

@toreamun
Copy link

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
Copy link

7amou3 commented Sep 2, 2023

Hello
Same for me
any workaround?

@TWhidden
Copy link

TWhidden commented Nov 7, 2023

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
Copy link

TWhidden commented Nov 7, 2023

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
Copy link

Pablissimo commented Dec 1, 2023

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
Copy link

Havunen commented Apr 14, 2024

This works in DotSwashbuckle, tested in 3.0.9

@martincostello martincostello added needs investigation help-wanted A change up for grabs for contributions from the community and removed needs investigation labels Apr 14, 2024
martincostello added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue Apr 14, 2024
Pass the `Type` value to `JsonSerializer` to give a hint as to how to serialize the value.
See domaindrivendev#2593 (comment).
martincostello added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue Apr 14, 2024
Use JsonOptions for Minimal APIs in .NET 8+ if not available from MVC.
Use `JsonSerializerOptions.Default` where available.
See domaindrivendev#2593 (comment).
@martincostello
Copy link
Collaborator

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 #2593 (comment) to #2800.

The reason #2593 (comment) 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 added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue Apr 16, 2024
Use JsonOptions for Minimal APIs in .NET 8+ if not available from MVC.
Use `JsonSerializerOptions.Default` where available.
See domaindrivendev#2593 (comment).
martincostello added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue Apr 16, 2024
Pass the `Type` value to `JsonSerializer` to give a hint as to how to serialize the value.
See domaindrivendev#2593 (comment).
martincostello added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue Apr 16, 2024
Pass the `Type` value to `JsonSerializer` to give a hint as to how to serialize the value.
See domaindrivendev#2593 (comment).
martincostello added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue Apr 23, 2024
Use JsonOptions for Minimal APIs in .NET 8+ if not available from MVC.
Use `JsonSerializerOptions.Default` where available.
See domaindrivendev#2593 (comment).
martincostello added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue Apr 23, 2024
Pass the `Type` value to `JsonSerializer` to give a hint as to how to serialize the value.
See domaindrivendev#2593 (comment).
martincostello added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue May 1, 2024
Use JsonOptions for Minimal APIs in .NET 8+ if not available from MVC.
Use `JsonSerializerOptions.Default` where available.
See domaindrivendev#2593 (comment).
martincostello added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue May 1, 2024
Pass the `Type` value to `JsonSerializer` to give a hint as to how to serialize the value.
See domaindrivendev#2593 (comment).
martincostello added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue May 8, 2024
Use JsonOptions for Minimal APIs in .NET 8+ if not available from MVC.
Use `JsonSerializerOptions.Default` where available.
See domaindrivendev#2593 (comment).
martincostello added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue May 8, 2024
Pass the `Type` value to `JsonSerializer` to give a hint as to how to serialize the value.
See domaindrivendev#2593 (comment).
martincostello added a commit to martincostello/Swashbuckle.AspNetCore that referenced this issue May 12, 2024
Pass the `Type` value to `JsonSerializer` to give a hint as to how to serialize the value.
See domaindrivendev#2593 (comment).
@reichigo
Copy link

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
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help-wanted A change up for grabs for contributions from the community
Projects
None yet
Development

No branches or pull requests

8 participants