Open ripvannwinkler opened 2 years ago
Looking into this some more, it looks like the problem may be in SwaggerGenServiceCollectionExtensions.cs, line 34:
#if (!NETSTANDARD2_0)
var serializerOptions = s.GetService<IOptions<JsonOptions>>()?.Value?.JsonSerializerOptions
?? new JsonSerializerOptions();
#else
var serializerOptions = new JsonSerializerOptions();
#endif
The problem here is that the JsonOptions type is pulled from the Microsoft.AspNetCore.Mvc
namespace while .NET 6 minimal APIs are documented to use JsonOptions from the Microsoft.AspNetCore.Http.Json
namespace which this code seems blissfully unaware of. Not sure what the path forward is.
I ran into the same problem and my colleague suggested the following as a workaround, which works for me:
using Microsoft.AspNetCore.Http.Json;
using MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions;
.....
builder.Services.Configure<JsonOptions>(o => o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
builder.Services.Configure<MvcJsonOptions>(o => o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
@RCSandberg yep, I ended up configuring both. I'm not sure there's a better way without added complexity. Microsoft has a recent history of duplicating entire namespaces for new framework/features. I get it for compatibility reasons, but it makes things like this a real pain.
You can always use NewtonsoftJson
it is in nuget <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.2" />
here are my settings
.AddNewtonsoftJson(options => { options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; options.SerializerSettings.Converters.Add(new StringEnumConverter()); options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; });
I ran into the same problem and my colleague suggested the following as a workaround, which works for me:
using Microsoft.AspNetCore.Http.Json; using MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions; ..... builder.Services.Configure<JsonOptions>(o => o.SerializerOptions.Converters.Add(new JsonStringEnumConverter())); builder.Services.Configure<MvcJsonOptions>(o => o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
Is this workaround working on version net6.0 (6.0.100)? Dosen't seem to work for me. Still get a serializer exception. Using System.Text.Json 6.0.2
An unhandled exception has occurred while executing the request. Microsoft.AspNetCore.Http.BadHttpRequestException: Failed to read parameter "MessageRequest messageRequest" from the request body as JSON. ---> System.Text.Json.JsonException: The JSON value could not be converted to Domain.Shared.Enums.MessageTypeEnum. Path: $.messageType | LineNumber: 2 | BytePositionInLine: 28.
For now im passing in HttpRequest httpRequest
and deserializing it myself, which is not ideal/clean.
To anyone facing a similar issue as me, make sure you have the right namespace when registerting your serializer globally.
NOT - Microsoft.AspNetCore.Mvc - JsonOptions YES - Microsoft.AspNetCore.Http.Json - JsonOptions
It was in the docs but i missed it when migrating to minimal api's since the namings are ver similar.
This has worked for me:
using System.Text.Json.Serialization;
builder.Services.Configure
[https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-6.0]
Swashbuckle.AspNetCore 6.4.0 picks up changes from "Microsoft.AspNetCore.Mvc.JsonOptions" options. However, minimal APIs are using "Microsoft.AspNetCore.Http.Json.JsonOptions". Therefore, both of them needs to be configured for Swagger schemas to match the API output.
It would be nice if Swashbuckle can be configured to use "Microsoft.AspNetCore.Http.Json.JsonOptions".
In RicoSuter/NSwag
got same problem,
Fixed with downgrading to
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.0" />
with <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
this bug fires!
@wrkntwrkn and @stvwndr helped lead me to the workaround that I'm using:
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
{
options.SerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
});
builder.Services.Configure<Microsoft.AspNetCore.Mvc.JsonOptions>(options =>
{
options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
});
@wrkntwrkn and @stvwndr helped lead me to the workaround that I'm using:
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options => { options.SerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); }); builder.Services.Configure<Microsoft.AspNetCore.Mvc.JsonOptions>(options => { options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); });
Thank you, this works as expected!
The below workaround works on the swagger schema.
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
{
options.SerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
});
builder.Services.Configure<Microsoft.AspNetCore.Mvc.JsonOptions>(options =>
{
options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
});
However, I had to use AddContollers
to get it working with the swagger example.
builder.Services.AddControllers()
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
Has anyone been able to get it working with the swagger example without adding AddControllers
?
No luck for me on either front.
srv.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
{
options.SerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
});
srv.Configure<Microsoft.AspNetCore.Mvc.JsonOptions>(options =>
{
options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
});
Swagger still shows enums as integers:
Here is how I add Swagger:
private static IServiceCollection AddSwagger(this IServiceCollection srv)
{
srv.AddEndpointsApiExplorer();
srv.AddSwaggerGen(
options =>
{
options.MapType<decimal>(() => new OpenApiSchema { Type = "number", Format = "decimal" });
options.AddSecurityDefinition(
JwtBearerDefaults.AuthenticationScheme,
new OpenApiSecurityScheme
{
Description =
"JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = JwtBearerDefaults.AuthenticationScheme
});
options.AddSecurityRequirement(
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme, Id = JwtBearerDefaults.AuthenticationScheme
},
Scheme = "oauth2",
Name = JwtBearerDefaults.AuthenticationScheme,
In = ParameterLocation.Header,
},
new List<string>()
}
});
options.SwaggerDoc(Constants.SwaggerDocVersion,
new OpenApiInfo
{
Title = "TrailheadTechnology API", Version = Constants.SwaggerDocVersion, Description = "Documents the REST API"
});
options.IncludeXmlComments(Assembly.GetExecutingAssembly().Location.Replace("dll", "xml"), true);
var docPath = typeof(TokenRequest).Assembly.Location.Replace("dll", "xml");
options.IncludeXmlComments(docPath);
options.SchemaFilter<EnumTypesSchemaFilter>(docPath);
options.EnableAnnotations();
}).AddSwaggerGenNewtonsoftSupport(); // explicit opt-in
return srv;
}
Where
public class EnumTypesSchemaFilter : ISchemaFilter
{
private readonly XDocument _xmlComments;
public EnumTypesSchemaFilter(string xmlPath)
{
if (File.Exists(xmlPath))
{
_xmlComments = XDocument.Load(xmlPath);
}
}
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (_xmlComments == null) return;
if (schema.Enum != null && schema.Enum.Count > 0 &&
context.Type != null && context.Type.IsEnum)
{
schema.Description += "<p>Members:</p><ul>";
var fullTypeName = context.Type.FullName;
foreach (var enumMemberName in schema.Enum.OfType<OpenApiString>().Select(v => v.Value))
{
var fullEnumMemberName = $"F:{fullTypeName}.{enumMemberName}";
var enumMemberComments = _xmlComments.Descendants("member")
.FirstOrDefault(m => m.Attribute("name").Value.Equals(fullEnumMemberName, StringComparison.OrdinalIgnoreCase));
if (enumMemberComments == null) continue;
var summary = enumMemberComments.Descendants("summary").FirstOrDefault();
if (summary == null) continue;
schema.Description += $"<li><i>{enumMemberName}</i> - {summary.Value.Trim()}</li>";
}
schema.Description += "</ul>";
}
}
}
I'm using .Net7, and this issue still exists. Here's my project setup:
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
Here's my code:
public record DemoObject
{
[JsonConverter(typeof(JsonStringEnumConverter))]
public DemoEnum DemoProperty { get; init; }
}
public enum DemoEnum : int { PlanA = 9, PlanB = 13 }
[Route("api/[controller]")]
[ApiController]
public class DemoController : ControllerBase
{
[HttpGet("demo")]
public DemoObject DemoAction() => new() { DemoProperty = DemoEnum.PlanB };
}
As you can see, I decorated enum field with JsonConverterAttribute
, using JsonStringEnumConverter
as attribute parameter.
If I hit "execute", I can get PlanB
in response as expected.
However, when generating "Example value", the json serializer serialized enum field as integer, as the screenshot below demonstrates.
I'm using .Net7, and this issue still exists. Here's my project setup:
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
Here's my code:
public record DemoObject { [JsonConverter(typeof(JsonStringEnumConverter))] public DemoEnum DemoProperty { get; init; } } public enum DemoEnum : int { PlanA = 9, PlanB = 13 } [Route("api/[controller]")] [ApiController] public class DemoController : ControllerBase { [HttpGet("demo")] public DemoObject DemoAction() => new() { DemoProperty = DemoEnum.PlanB }; }
As you can see, I decorated enum field with
JsonConverterAttribute
, usingJsonStringEnumConverter
as attribute parameter. If I hit "execute", I can getPlanB
in response as expected. However, when generating "Example value", the json serializer serialized enum field as integer, as the screenshot below demonstrates.
I can confirm that the issue still persists for me too.
I managed to get this fully working for minimal API scenarios by decorating the enum type itself with JsonConverterAttribute.
Tested on NET7 and NET8 RC1:
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
// var builder = WebApplication.CreateSlimBuilder(args); -NET8-
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// OK
app.MapPost("postDemo", (RequestDto dto) => TypedResults.Ok(new ResponseDto(dto.Enum)));
// Does not work, query param schema is string
app.MapGet("getDemo1", (DemoEnum? enumParameter) => TypedResults.Ok(new ResponseDto(enumParameter)));
// OK
app.MapGet("getDemo2", ([FromQuery] DemoEnum? enumParameter) => TypedResults.Ok(new ResponseDto(enumParameter)));
// OK
app.MapGet("getDemo3", ([AsParameters] Parameters parameters) => TypedResults.Ok(new ResponseDto(parameters.EnumParameter)));
app.UseSwagger();
app.UseSwaggerUI();
app.Run();
public struct Parameters
{
[FromQuery]
public DemoEnum? EnumParameter { get; set; }
}
public sealed record RequestDto(DemoEnum? Enum);
public sealed record ResponseDto(DemoEnum? Enum);
// [JsonConverter(typeof(JsonStringEnumConverter<DemoEnum>))] -NET8-
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum DemoEnum
{
FirstValue,
SecondValue,
}
[JsonConverter(typeof(JsonStringEnumConverter))] public enum DemoEnum
Thanks this works for me. But of course decorating all enums with that is far from ideal, it would be nice if ASPNET respected the serializer converter.
I managed to get this fully working for minimal API scenarios by decorating the enum type itself with JsonConverterAttribute.
Tested on NET7 and NET8 RC1:
using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; // var builder = WebApplication.CreateSlimBuilder(args); -NET8- var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // OK app.MapPost("postDemo", (RequestDto dto) => TypedResults.Ok(new ResponseDto(dto.Enum))); // Does not work, query param schema is string app.MapGet("getDemo1", (DemoEnum? enumParameter) => TypedResults.Ok(new ResponseDto(enumParameter))); // OK app.MapGet("getDemo2", ([FromQuery] DemoEnum? enumParameter) => TypedResults.Ok(new ResponseDto(enumParameter))); // OK app.MapGet("getDemo3", ([AsParameters] Parameters parameters) => TypedResults.Ok(new ResponseDto(parameters.EnumParameter))); app.UseSwagger(); app.UseSwaggerUI(); app.Run(); public struct Parameters { [FromQuery] public DemoEnum? EnumParameter { get; set; } } public sealed record RequestDto(DemoEnum? Enum); public sealed record ResponseDto(DemoEnum? Enum); // [JsonConverter(typeof(JsonStringEnumConverter<DemoEnum>))] -NET8- [JsonConverter(typeof(JsonStringEnumConverter))] public enum DemoEnum { FirstValue, SecondValue, }
Thanks, this also works in NET6!
Just noting that another approach here is the below lines:
builder.Services.AddTransient<ISerializerDataContractResolver>(_ => new JsonSerializerDataContractResolver(
new JsonSerializerOptions {
// set whatever default options for JSON serialization you want like enums, PropertyNamingPolicy, or whatever
}
));
I think the comment at https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2293#issuecomment-991870685 is correct that this is the bug because of this code: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/8f363f7359cb1cb8fa5de5195ec6d97aefaa16b3/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs#L33-L43
The code in the #else
block just uses the default JsonSerializerOptions
if the code isn't NETSTANDARD2_0
which includes net8.0.
The following method was used to collect the Serializer settings in one place.
builder.Services.TryAddTransient<ISerializerDataContractResolver>(s =>
{
var httpJsonOptions = s.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>();
var serializerOptions = httpJsonOptions.Value.SerializerOptions;
return new JsonSerializerDataContractResolver(serializerOptions);
});
builder.Services.AddSwaggerGen(options =>
{
// ...
});
Just checked with the latest Swashbuckle packages versions and this issue still relevant. Configuring both "classic" and "minimal" JsonOptions
suggested above works, yet a bit bothering to have.
The following method was used to collect the Serializer settings in one place.
builder.Services.TryAddTransient<ISerializerDataContractResolver>(s => { var httpJsonOptions = s.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>(); var serializerOptions = httpJsonOptions.Value.SerializerOptions; return new JsonSerializerDataContractResolver(serializerOptions); }); builder.Services.AddSwaggerGen(options => { // ... });
It works. Thanks
This issue is stale because it has been open for 60 days with no activity. It will be automatically closed in 14 days if no further updates are made.
A comment to prevent this one from closing due to inactivity, as I believe it's still applicable and does have all the information for the reproduction.
Looking into this some more, it looks like the problem may be in SwaggerGenServiceCollectionExtensions.cs, line 34:
#if (!NETSTANDARD2_0) var serializerOptions = s.GetService<IOptions<JsonOptions>>()?.Value?.JsonSerializerOptions ?? new JsonSerializerOptions(); #else var serializerOptions = new JsonSerializerOptions(); #endif
The problem here is that the JsonOptions type is pulled from the
Microsoft.AspNetCore.Mvc
namespace while .NET 6 minimal APIs are documented to use JsonOptions from theMicrosoft.AspNetCore.Http.Json
namespace which this code seems blissfully unaware of. Not sure what the path forward is.
Maybe the best option would be to have something like this:
builder.Services.AddSwaggerGen(options => options.EnableMinimalApi());
Isn't it because AspNetCore.Mvc.JsonOptions.JsonSerializerOptions
is never null?
https://github.com/dotnet/dotnet/blob/main/src/aspnetcore/src/Mvc/Mvc.Core/src/JsonOptions.cs#L35
#if !NETSTANDARD2_0
serializerOptions =
_serviceProvider.GetService<IOptions<AspNetCore.Mvc.JsonOptions>>()?.Value?.JsonSerializerOptions
#if NET8_0_OR_GREATER
?? _serviceProvider.GetService<IOptions<AspNetCore.Http.Json.JsonOptions>>()?.Value?.SerializerOptions
#endif
#if NET7_0_OR_GREATER
?? JsonSerializerOptions.Default;
#else
?? new JsonSerializerOptions();
#endif
This code will never get SerializerOptions from AspNetCore.Http.Json.JsonOptions
.
Using Swashbuckle.AspNetCore 6.2.3 with ASP.NET 6 minimal API, I have configured JSON serializer options per the docs.
However, Swagger UI still reports enums as ints unless I add also use AddControllers().AddJsonOptions():
Once the second block is added, the swagger UI output references the enum string values as expected. As far as I know, the latter simply does the former under the hood, but there seems to be some order of operations affecting the schema generation here?