Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,20 @@ In versions of Swashbuckle.AspNetCore prior to `5.0.0`, Swashbuckle would genera
on the behavior of the [Newtonsoft.Json serializer][newtonsoft-json]. This made sense because that was the serializer that shipped with ASP.NET Core
at the time. However, since ASP.NET Core 3.0, ASP.NET Core introduces a new serializer, [System.Text.Json (STJ)][system-text-json] out-of-the-box.

If you find that the *STJ* options/attributes are not being honored, this may be because you are using a combination of Minimal APIs and MVC, which have separate JSON options.
To force the OpenAPI document generation to use either set of JSON options you can use one of the following methods:

```csharp
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});
// Then either:
services.AddSwaggerGenMinimalApisJsonOptions();
// or ...
services.AddSwaggerGenMvcJsonOptions();
```

If you want to use Newtonsoft.Json instead, you need to install a separate package and explicitly opt-in. By default Swashbuckle.AspNetCore will assume
you're using the System.Text.Json serializer and generate Schemas based on its behavior. If you're using Newtonsoft.Json then you'll need to install a
separate Swashbuckle package, [Swashbuckle.AspNetCore.Newtonsoft][swashbuckle-aspnetcore-newtonsoft] to explicitly opt-in.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.Options;

namespace Swashbuckle.AspNetCore.SwaggerGen.DependencyInjection;

internal sealed class ConfigureMinimalApiSwaggerGenJsonOptions(IOptions<JsonOptions> jsonOptions) : IConfigureOptions<SwaggerGenJsonOptions>
{
public void Configure(SwaggerGenJsonOptions options)
{
options.SerializerOptions = jsonOptions.Value.SerializerOptions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace Swashbuckle.AspNetCore.SwaggerGen.DependencyInjection;

internal sealed class ConfigureMvcSwaggerGenJsonOptions(IOptions<JsonOptions> jsonOptions) : IConfigureOptions<SwaggerGenJsonOptions>
{
public void Configure(SwaggerGenJsonOptions options)
{
options.SerializerOptions = jsonOptions.Value.JsonSerializerOptions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Text.Json;
using Microsoft.Extensions.Options;

namespace Swashbuckle.AspNetCore.SwaggerGen.DependencyInjection;

internal sealed class ConfigureSwaggerGenJsonOptions : IPostConfigureOptions<SwaggerGenJsonOptions>
{
private readonly IEnumerable<IConfigureOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>> _minimalApiConfigureOptions;
private readonly IEnumerable<IPostConfigureOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>> _minimalApiPostConfigureOptions;
private readonly Microsoft.AspNetCore.Http.Json.JsonOptions _minimalApiJsonOptions;
private readonly Microsoft.AspNetCore.Mvc.JsonOptions _mvcJsonOptions;

public ConfigureSwaggerGenJsonOptions(
IEnumerable<IConfigureOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>> minimalApiConfigureOptions,
IEnumerable<IPostConfigureOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>> minimalApiPostConfigureOptions,
IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions> minimalApiJsonOptions,
IOptions<Microsoft.AspNetCore.Mvc.JsonOptions> mvcJsonOptions)
{
_minimalApiConfigureOptions = minimalApiConfigureOptions;
_minimalApiPostConfigureOptions = minimalApiPostConfigureOptions;
_minimalApiJsonOptions = minimalApiJsonOptions.Value;
_mvcJsonOptions = mvcJsonOptions.Value;
}

public void PostConfigure(string name, SwaggerGenJsonOptions options)
{
if (options.SerializerOptions != null)
{
return;
}

/*
* There is no surefire way to do this.
* However, both JsonOptions are defaulted in the same way.
* If neither is configured it makes no difference which one is chosen.
* If both are configured, then we just need to make a choice.
* As Minimal APIs are newer if someone is configuring them
* it's probably more likely that is what they're using.
*
* If either JsonOptions is null we will try to create a new instance as
* a last resort as this is an expensive operation.
*/

var serializerOptions = _mvcJsonOptions.JsonSerializerOptions;

if (_minimalApiConfigureOptions.Any() || _minimalApiPostConfigureOptions.Any())
{
serializerOptions = _minimalApiJsonOptions.SerializerOptions;
}

options.SerializerOptions = serializerOptions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Text.Json;

namespace Swashbuckle.AspNetCore.SwaggerGen.DependencyInjection;

/// <summary>
/// Configures the <see cref="JsonSerializerOptions"/> to be used by <see cref="JsonSerializerDataContractResolver"/>.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have two implementations of ISerializerDataContractResolver (the other being NewtonsoftDataContractResolver), so rather than explicitly reference one in code, I would make this less specific.

Maybe something like:

A class to specify options to use to configure JSON serialization for OpenAPI documents.

/// </summary>
public class SwaggerGenJsonOptions
{
/// <summary>
/// Gets or sets the JSON serializer options to use.
/// </summary>
public JsonSerializerOptions SerializerOptions { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System.Text.Json;
using Microsoft.Extensions.ApiDescriptions;
using Microsoft.Extensions.ApiDescriptions;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerGen.DependencyInjection;

namespace Microsoft.Extensions.DependencyInjection;

Expand All @@ -29,10 +29,10 @@ public static IServiceCollection AddSwaggerGen(
services.TryAddTransient(s => s.GetRequiredService<IOptions<SwaggerGeneratorOptions>>().Value);
services.TryAddTransient<ISchemaGenerator, SchemaGenerator>();
services.TryAddTransient(s => s.GetRequiredService<IOptions<SchemaGeneratorOptions>>().Value);
services.AddSingleton<JsonSerializerOptionsProvider>();
services.ConfigureOptions<ConfigureSwaggerGenJsonOptions>();
services.TryAddSingleton<ISerializerDataContractResolver>(s =>
{
var serializerOptions = s.GetRequiredService<JsonSerializerOptionsProvider>().Options;
var serializerOptions = s.GetRequiredService<IOptions<SwaggerGenJsonOptions>>().Value.SerializerOptions;
return new JsonSerializerDataContractResolver(serializerOptions);
});

Expand All @@ -44,41 +44,20 @@ public static IServiceCollection AddSwaggerGen(
return services;
}

public static void ConfigureSwaggerGen(
this IServiceCollection services,
Action<SwaggerGenOptions> setupAction)
public static IServiceCollection AddSwaggerGenMinimalApisJsonOptions(this IServiceCollection services)
{
services.Configure(setupAction);
return services.ConfigureOptions<ConfigureMinimalApiSwaggerGenJsonOptions>();
}

private sealed class JsonSerializerOptionsProvider
public static IServiceCollection AddSwaggerGenMvcJsonOptions(this IServiceCollection services)
{
private JsonSerializerOptions _options;
private readonly IServiceProvider _serviceProvider;

public JsonSerializerOptionsProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public JsonSerializerOptions Options => _options ??= ResolveOptions();

private JsonSerializerOptions ResolveOptions()
{
JsonSerializerOptions serializerOptions;

/*
* First try to get the options configured for MVC,
* then try to get the options configured for Minimal APIs if available,
* then try the default JsonSerializerOptions if available,
* otherwise create a new instance as a last resort as this is an expensive operation.
*/
serializerOptions =
_serviceProvider.GetService<IOptions<AspNetCore.Mvc.JsonOptions>>()?.Value?.JsonSerializerOptions
?? _serviceProvider.GetService<IOptions<AspNetCore.Http.Json.JsonOptions>>()?.Value?.SerializerOptions
?? JsonSerializerOptions.Default;
return services.ConfigureOptions<ConfigureMvcSwaggerGenJsonOptions>();
}

return serializerOptions;
}
public static void ConfigureSwaggerGen(
this IServiceCollection services,
Action<SwaggerGenOptions> setupAction)
{
services.Configure(setupAction);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Swashbuckle.AspNetCore.SwaggerGen.DependencyInjection.SwaggerGenJsonOptions
Swashbuckle.AspNetCore.SwaggerGen.DependencyInjection.SwaggerGenJsonOptions.SerializerOptions.get -> System.Text.Json.JsonSerializerOptions
Swashbuckle.AspNetCore.SwaggerGen.DependencyInjection.SwaggerGenJsonOptions.SerializerOptions.set -> void
Swashbuckle.AspNetCore.SwaggerGen.DependencyInjection.SwaggerGenJsonOptions.SwaggerGenJsonOptions() -> void
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@

static Microsoft.Extensions.DependencyInjection.SwaggerGenServiceCollectionExtensions.AddSwaggerGenMinimalApisJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection
static Microsoft.Extensions.DependencyInjection.SwaggerGenServiceCollectionExtensions.AddSwaggerGenMvcJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@

static Microsoft.Extensions.DependencyInjection.SwaggerGenServiceCollectionExtensions.AddSwaggerGenMinimalApisJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection
static Microsoft.Extensions.DependencyInjection.SwaggerGenServiceCollectionExtensions.AddSwaggerGenMvcJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection
Original file line number Diff line number Diff line change
Expand Up @@ -988,12 +988,11 @@
},
"DateTimeKind": {
"enum": [
0,
1,
2
"Unspecified",
"Utc",
"Local"
],
"type": "integer",
"format": "int32"
"type": "string"
},
"Fruit": {
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -988,12 +988,11 @@
},
"DateTimeKind": {
"enum": [
0,
1,
2
"Unspecified",
"Utc",
"Local"
],
"type": "integer",
"format": "int32"
"type": "string"
},
"Fruit": {
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -988,12 +988,11 @@
},
"DateTimeKind": {
"enum": [
0,
1,
2
"Unspecified",
"Utc",
"Local"
],
"type": "integer",
"format": "int32"
"type": "string"
},
"Fruit": {
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -988,12 +988,11 @@
},
"DateTimeKind": {
"enum": [
0,
1,
2
"Unspecified",
"Utc",
"Local"
],
"type": "integer",
"format": "int32"
"type": "string"
},
"Fruit": {
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;

namespace Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures;

internal sealed class DummyHostEnvironment : IWebHostEnvironment
{
public string WebRootPath { get; set; }
public IFileProvider WebRootFileProvider { get; set; }
public string ApplicationName { get; set; }
public IFileProvider ContentRootFileProvider { get; set; }
public string ContentRootPath { get; set; }
public string EnvironmentName { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen.DependencyInjection;
using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures;

using MinimalApiJsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;
using MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions;

namespace Swashbuckle.AspNetCore.SwaggerGen.Test;

public class SwaggerGenJsonOptionsTests
{
[Fact]
public static void Ensure_SwaggerGenJsonOptions_Uses_MinimalApi_JsonOptions_When_Overridden()
{
var services = new ServiceCollection();
services.AddSingleton<IWebHostEnvironment, DummyHostEnvironment>();
services.AddSwaggerGen();
services.AddMvcCore().AddJsonOptions(o => o.JsonSerializerOptions.AllowTrailingCommas = true);
services.AddSwaggerGenMinimalApisJsonOptions();

using var provider = services.BuildServiceProvider();

var minimalApiJsonOptions = provider.GetService<IOptions<MinimalApiJsonOptions>>().Value.SerializerOptions;
var swaggerGenSerializerOptions = provider.GetService<IOptions<SwaggerGenJsonOptions>>().Value.SerializerOptions;
Assert.Equal(minimalApiJsonOptions, swaggerGenSerializerOptions);
}

[Fact]
public static void Ensure_SwaggerGenJsonOptions_Uses_Mvc_JsonOptions_When_Overridden()
{
var services = new ServiceCollection();
services.AddSingleton<IWebHostEnvironment, DummyHostEnvironment>();
services.AddSwaggerGen();
services.ConfigureHttpJsonOptions(o => o.SerializerOptions.AllowTrailingCommas = true);
services.AddSwaggerGenMvcJsonOptions();

using var provider = services.BuildServiceProvider();

var mvcJsonOptions = provider.GetService<IOptions<MvcJsonOptions>>().Value.JsonSerializerOptions;
var swaggerGenSerializerOptions = provider.GetService<IOptions<SwaggerGenJsonOptions>>().Value.SerializerOptions;
Assert.Equal(mvcJsonOptions, swaggerGenSerializerOptions);
}

[Fact]
public static void Ensure_SwaggerGenJsonOptions_Uses_Mvc_JsonOptions_When_Not_Using_Minimal_Apis()
{
var services = new ServiceCollection();
services.AddSingleton<IWebHostEnvironment, DummyHostEnvironment>();
services.AddSwaggerGen();
services.AddMvcCore().AddJsonOptions(o => o.JsonSerializerOptions.AllowTrailingCommas = true);

using var provider = services.BuildServiceProvider();

var mvcJsonOptions = provider.GetService<IOptions<MvcJsonOptions>>().Value.JsonSerializerOptions;
var swaggerGenSerializerOptions = provider.GetService<IOptions<SwaggerGenJsonOptions>>().Value.SerializerOptions;
Assert.Equal(mvcJsonOptions, swaggerGenSerializerOptions);
}

[Fact]
public static void Ensure_SwaggerGenJsonOptions_Uses_MinimalApi_JsonOptions_When_Configured()
{
var services = new ServiceCollection();
services.AddSingleton<IWebHostEnvironment, DummyHostEnvironment>();
services.AddSwaggerGen();
services.ConfigureHttpJsonOptions(o => o.SerializerOptions.AllowTrailingCommas = true);
services.AddMvcCore().AddJsonOptions(o => o.JsonSerializerOptions.AllowTrailingCommas = true);

using var provider = services.BuildServiceProvider();

var minimalApiJsonOptions = provider.GetService<IOptions<MinimalApiJsonOptions>>().Value.SerializerOptions;
var swaggerGenSerializerOptions = provider.GetService<IOptions<SwaggerGenJsonOptions>>().Value.SerializerOptions;
Assert.Equal(minimalApiJsonOptions, swaggerGenSerializerOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures;

namespace Swashbuckle.AspNetCore.SwaggerGen.Test;

Expand Down Expand Up @@ -179,14 +179,4 @@ public void Ensure_IncludeXmlComments_Adds_Filter_To_Options()
Assert.NotNull(options);
Assert.Contains(options.DocumentFilters, x => x is XmlCommentsDocumentFilter);
}

private sealed class DummyHostEnvironment : IWebHostEnvironment
{
public string WebRootPath { get; set; }
public IFileProvider WebRootFileProvider { get; set; }
public string ApplicationName { get; set; }
public IFileProvider ContentRootFileProvider { get; set; }
public string ContentRootPath { get; set; }
public string EnvironmentName { get; set; }
}
}
Loading
Loading