Swashbuckle.AspNetCore
Swashbuckle.AspNetCore copied to clipboard
Setting `AdditionalPropertiesAllowed` For Properties Is Not Reflected In Generated Swagger JSON
Background
We have an API which publishes it's contract as on OpenAPI doc using Swashbuckle 6.5.0. One route on that API publishes a dynamic javascript hash object which we define as having type object
. We would like to use a SchemaFilter
to then add "additionalProperties": true
to the document for such properties. However, setting AdditionalPropertiesAllowed
does not add the additionalProperties
field as expected.
Note, setting AdditionalProperties
does work as expected but is not a solution in this case since the hash is schemaless. For instance, specifying schema.AdditionalProperties = new OpenApiSchema()
yields "additionalProperties": {}
in the resulting document.
Affected Version
<TargetFramework>net6.0</TargetFramework>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.5.0" />
Steps to Reproduce
- Create a
SchemaFilter
that setsAdditionalPropertiesAllowed = true
for any property of an object schema
Expected Behavior
Property is marked as "additionalProperties": true
in generated OpenAPI document
Actual Behavior
Property has no additionalProperties
field at all in generated OpenAPI document
Test Case
Below is a test file with a single failing test which demonstrates the issue:
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Xunit;
namespace Swashbuckle.AspNetCore.IntegrationTests;
public class SwaggerGenIntegrationTests
{
[Fact]
public async Task SchemaFilter_WhenSchemaFilterSetsAdditionalPropertiesAllowedToTrue_ThenGeneratedOpenApiReflectsTheSame()
{
// arrange
using var server = CreateTestApiServer();
using var client = server.CreateClient();
// act
var openApiJson = await client.GetStringAsync("/swagger/v1/swagger.json");
// assert
Assert.NotNull(openApiJson);
var openApiDoc = JsonDocument.Parse(openApiJson).RootElement;
var modelSchema =
openApiDoc
.GetProperty("components")
.GetProperty("schemas")
.GetProperty(nameof(GetResponseModel));
Assert.Contains("\"additionalProperties\": false", modelSchema.ToString()); // passes; top-level unaffected
var modelPropertiesSchema =
modelSchema
.GetProperty("properties");
Assert.Contains("\"additionalProperties\": true", modelPropertiesSchema.ToString()); // fails
}
internal TestServer CreateTestApiServer() =>
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<TestApiStartup>();
builder.UseTestServer();
})
.Start()
.GetTestServer();
}
public class AdditionalPropertiesSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context?.Type == typeof(object))
{
schema.AdditionalPropertiesAllowed = true;
}
}
}
#region TestApiClasses
// public because SwaggerGen needs access
public class GetResponseModel
{
public object Object { get; init; } = new { };
public dynamic Dynamic { get; init; } = new { };
}
// public because SwaggerGen needs access
[ApiController]
public class ExampleController : ControllerBase
{
[HttpGet]
[Route("example")]
public ActionResult<GetResponseModel> GetExample() =>
Ok(new GetResponseModel
{
Object = new { Key = "value" },
Dynamic = new { Nested = new { Inner = true } },
});
}
internal class TestApiStartup
{
public void ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddControllers();
serviceCollection.AddEndpointsApiExplorer();
serviceCollection.AddSwaggerGen(genOptions =>
genOptions.SchemaFilter<AdditionalPropertiesSchemaFilter>());
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(routing => routing.MapControllers());
app.UseSwagger();
app.UseSwaggerUI();
}
}
#endregion
I'm pretty sure the issue is a disconnect between the OpenAPI spec and Microsoft's OpenApiDocument implementation.
Swashbuckle delegates the actual document serialization to Microsoft.OpenApi.Models.OpenApiDocument:
if (_options.SerializeAsV2) swagger.SerializeAsV2(jsonWriter); else swagger.SerializeAsV3(jsonWriter);
That SerializeAsV3 method will not output additionalProperties: true
. You can review that in its repo.
A tip in the Swagger.io docs site has this (sorry, no direct link):
So, to get additionalProperties
into the document, the OpenApiSchema object must have a non-null value for its AdditionalProperties property.
schema.AdditionalPropertiesAllowed = true;
schema.AdditionalProperties = new();
I used an attribute on a class to indicate to the filter that it should update the schema. Details in the GitHub gist: AdditionalPropertiesSchemaFilter
I'm pretty sure the issue is a disconnect between the OpenAPI spec and Microsoft's OpenApiDocument implementation.
I agree. We in fact addressed this shortcoming exactly as you suggest in our filter.
So, to get additionalProperties into the document, the OpenApiSchema object must have a non-null value for its AdditionalProperties property.
schema.AdditionalPropertiesAllowed = true; schema.AdditionalProperties = new();
That said, I think it is mighty confusing for users of this library who see schema.AdditionalPropertiesAllowed = false;
work like they expect but who must then set a completely separate property when trying to make schema.AdditionalPropertiesAllowed = true;
work. My recommendation would be that schema.AdditionalProperties
have some logic so that users don't have to deal directly with this implementation detail, which I expect would be something along the lines of:
schema.AdditionalProperties ??= schema.AdditionalPropertiesAllowed ? new() : null;
EDIT: If I have time, I will try to figure out the best place for this logic and put up a PR but since we addressed this for ourselves a while ago, I unfortunately may not get to it very quickly.