Open
Description
Summary
Using Microsoft.AspNetCore.OpenAPI version 9.0.6 creates duplicate schema definitions if the following conditions are met:
- Model: ApiResponse<T>: A generic wrapper to be used as endpoint response types.
- ModelA: A simple model
- ModelB: Another simple model that has ModelA as a property
- Controller endpoints:
a. EndpointA: Return type of ApiResponse<ModelA>
a. EndpointB: Return type of ApiResponse<ModelB>
When Generating the OpenAPI .json definition, ModelA will have 2 schema definitions:
- ModelA: This represents the return type from 4.a
- ModelA2: This represents the return type from 4.b (Note: ModelB will have a ref to #/components/schemas/ModelA)
Motivation and goals
As of net9.0, support for Swagger (NSwag) is deprecated in favour of OpenAPI. We are rewriting our API documentation layer to use OpenAPI so that we can transition to LTS version net10.0. However, using OpenAPI has the following effect:
Using the OpenAPI.json definition described above, our team generates an API Client for service consumers. As a result:
- There a hundreds more lines of .json and API Client .cs code.
- Previously, the API Client (generated from Swagger 2.0), would aggressively de-duplicate the schemas to ensure 1 schema definition for a singular DTO / model.
The extra Schemas are a breaking change as our API consumers will now need to react to:
- Changing from ModelA to ModelAx
- It is programmatically cumbersome to determine which variant of schema to use in code.
Minimum Repro steps:
.csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OpenApi" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" />
</ItemGroup>
</Project>
Program.cs:
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();
WebApplication app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Models / DTOS:
public class ApiResponse<T>
{
public T? Result { get; set; }
}
public class SingleItem
{
public string Name { get; set; } = string.Empty;
}
public class CollectionItems
{
public IList<SingleItem> Items { get; set; } = new List<SingleItem>();
}
Controller:
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet]
public ActionResult<ApiResponse<SingleItem>> GetSingleItem()
{
return new ApiResponse<SingleItem>
{
Result = new SingleItem { Name = "Test" }
};
}
[HttpGet]
public ActionResult<ApiResponse<CollectionItems>> GetCollectionItems()
{
return new ApiResponse<CollectionItems>
{
Result = new CollectionItems()
};
}
}
This results in:
{
"openapi": "3.0.4",
"info": {
"title": "OpenApi.DeduplicateSchemas | v1",
"version": "1.0.0"
},
"paths": {
"/api/Test": {
"get": {
"tags": [
"Test"
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ApiResponseOfCollectionItems"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResponseOfCollectionItems"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ApiResponseOfCollectionItems"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"ApiResponseOfCollectionItems": {
"type": "object",
"properties": {
"result": {
"$ref": "#/components/schemas/CollectionItems"
}
}
},
"ApiResponseOfSingleItem": {
"type": "object",
"properties": {
"result": {
"$ref": "#/components/schemas/SingleItem"
}
}
},
"CollectionItems": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SingleItem2"
}
}
},
"nullable": true
},
"SingleItem": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
}
},
"nullable": true
},
"SingleItem2": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
}
}
}
}
},
"tags": [
{
"name": "Test"
}
]
}
Similar issues:
- Fix self-referential schema handling for collection-based types #60339
- .NET 9 OpenAPI produces lots of duplicate schemas for the same object #58968 (comment) - This post indicates that net10.0 will resolve the scenario in the issue and that the fix will be back ported to Microsoft.AspNetCore.OpenAPI 9. I have upgraded to Microsoft.AspNetCore.OpenAPI 9.0.6 (The latest version) and my issue remains.