Skip to content

ASP.NET Core OpenAPI generator incorrectly duplicates schemas and creates invalid $ref for simple structure #60164

Closed
@Berreip

Description

@Berreip

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Description

The OpenAPI generator in ASP.NET Core produces an incorrect schema when a DTO contains multiple properties of the same type.
Instead of reusing the same schema, it creates a duplicate (ChangesetIntDto2). Additionally, it generates an invalid $ref, making the OpenAPI document invalid and unusable.

This occurs in both classic and minimal ASP.NET Core project using Microsoft.Extensions.ApiDescription.Server for OpenAPI generation.


Steps to reproduce

1. Minimal Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();
app.UseHttpsRedirection();

app.MapGet("/repro", () =>
{
    return Enumerable.Range(1, 5).Select(_ =>
        new ChangesetDescDto
        {
            Prop1 = new ChangesetIntDto { Added = [] },
            Prop2 = new ChangesetIntDto { Added = [] },
        }).ToArray();
}).WithName("reproendpoint");

app.Run();

2. DTOs

public sealed class ChangesetDescDto
{
    [JsonPropertyName("value1")]
    public required ChangesetIntDto Prop1 { get; init; }

    [JsonPropertyName("value2")]
    public required ChangesetIntDto Prop2 { get; init; }
}

public sealed class ChangesetIntDto
{
    [JsonPropertyName("added")]
    public required List<int> Added { get; init; }
}

3. Project Configuration (.csproj)

Using Microsoft.Extensions.ApiDescription.Server with OpenAPI document generation:

<!-- For build time OpenAPI JSON generation -->
<PropertyGroup>
    <OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
    <OpenApiGenerateDocumentsOptions>--file-name repro-open-api</OpenApiGenerateDocumentsOptions>
</PropertyGroup>

4. Generate OpenAPI JSON

After running the project and generating the OpenAPI document, the following incorrect schema is produced.


Expected behavior

  • Both value1 and value2 should reference ChangesetIntDto.
  • No duplicate schema (ChangesetIntDto2).
  • No invalid $ref values.

Actual behavior

  • value1 correctly references ChangesetIntDto, but value2 references ChangesetIntDto2 (a duplicated schema).
  • An invalid $ref is generated:
    "$ref": "#/components/schemas/#/items/properties/value1/properties/added"

Example of incorrect OpenAPI output

"components": {
  "schemas": {
    "ChangesetDescDto": {
      "required": ["value1", "value2"],
      "type": "object",
      "properties": {
        "value1": {
          "$ref": "#/components/schemas/ChangesetIntDto"
        },
        "value2": {
          "$ref": "#/components/schemas/ChangesetIntDto2"
        }
      }
    },
    "ChangesetIntDto": {
      "required": ["added"],
      "type": "object",
      "properties": {
        "added": {
          "type": "array",
          "items": {
            "type": "integer",
            "format": "int32"
          }
        }
      }
    },
    "ChangesetIntDto2": {
      "required": ["added"],
      "type": "object",
      "properties": {
        "added": {
          "$ref": "#/components/schemas/#/items/properties/value1/properties/added"
        }
      }
    }
  }
}

Annoyance level :-)

  • Breaks OpenAPI validation due to an invalid $ref entry.
  • Makes the schema unusable for client code generation (TypeScript, C#).
  • Introduces unnecessary schema duplication, which is inefficient and confusing.

Environment

  • .NET 9
  • ASP.NET Core
  • OpenAPI generation using builder.Services.AddOpenApi()
  • Microsoft.Extensions.ApiDescription.Server
  • Build-time OpenAPI document generation enabled in .csproj

Thanks.

Steps To Reproduce

See following minimal specific -only for you ❤- repo for reproduction : very simple and focus only on this issue with minimal code. Just Build to regenerate repro-open-api.json (but it is included by default if needed)

https://github.com/Berreip/repro_openApi_issue

thanks :-)

.NET Version

9.0.102

Re-tested on :

  • 9.0.2

Anything else?

dotnet --info

SDK .NET :
Version: 9.0.102
Commit: cb83cd4923
Workload version: 9.0.100-manifests.43af17c7
MSBuild version: 17.12.18+ed8c6aec5

Environnement d'exécution :
OS Name: Windows
OS Version: 10.0.26100
OS Platform: Windows
RID: win-x64
Base Path: C:\Program Files\dotnet\sdk\9.0.102\

Charges de travail .NET installées :
[ios]
Source de l’installation: VS 17.9.34616.47
Version de manifeste: 18.0.9617/9.0.100
Chemin d'accès au Manifeste: C:\Program Files\dotnet\sdk-manifests\9.0.100\microsoft.net.sdk.ios\18.0.9617\WorkloadManifest.json
Type d'installation: Msi

[maui-windows]
Source de l’installation: VS 17.9.34616.47
Version de manifeste: 9.0.0/9.0.100
Chemin d'accès au Manifeste: C:\Program Files\dotnet\sdk-manifests\9.0.100\microsoft.net.sdk.maui\9.0.0\WorkloadManifest.json
Type d'installation: Msi

[wasm-tools]
Source de l’installation: VS 17.9.34616.47
Version de manifeste: 9.0.1/9.0.100
Chemin d'accès au Manifeste: C:\Program Files\dotnet\sdk-manifests\9.0.100\microsoft.net.workload.mono.toolchain.current\9.0.1\WorkloadManifest.json
Type d'installation: Msi

[android]
Source de l’installation: VS 17.9.34616.47
Version de manifeste: 35.0.7/9.0.100
Chemin d'accès au Manifeste: C:\Program Files\dotnet\sdk-manifests\9.0.100\microsoft.net.sdk.android\35.0.7\WorkloadManifest.json
Type d'installation: Msi

[maccatalyst]
Source de l’installation: VS 17.9.34616.47
Version de manifeste: 18.0.9617/9.0.100
Chemin d'accès au Manifeste: C:\Program Files\dotnet\sdk-manifests\9.0.100\microsoft.net.sdk.maccatalyst\18.0.9617\WorkloadManifest.json
Type d'installation: Msi

Configuré pour utiliser loose manifests lors de l’installation de nouveaux manifestes.

Host:
Version: 9.0.1
Architecture: x64
Commit: c8acea2262

.NET SDKs installed:
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.526 [C:\Program Files\dotnet\sdk]
5.0.416 [C:\Program Files\dotnet\sdk]
8.0.200 [C:\Program Files\dotnet\sdk]
8.0.206 [C:\Program Files\dotnet\sdk]
9.0.102 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
Microsoft.AspNetCore.All 2.1.30 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.30 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 8.0.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 9.0.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.30 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.36 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 8.0.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 9.0.1 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 6.0.36 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 8.0.6 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 9.0.1 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
x86 [C:\Program Files (x86)\dotnet]
registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
Not set

global.json file:
Not found

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcfeature-openapi

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions