Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
1 task done
Berreip opened this issue Feb 2, 2025 · 3 comments
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi

Comments

@Berreip
Copy link

Berreip commented Feb 2, 2025

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 even in a 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.

Expected Behavior

A single ChangesetIntDto and a valid openApi file usable for Nswag or other tools generation.

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 :-)

Exceptions (if any)

No response

.NET Version

9.0.102

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

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Feb 2, 2025
@martincostello martincostello added feature-openapi area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Feb 2, 2025
@CRidge
Copy link

CRidge commented Feb 5, 2025

I'm seeing the same behavior.

It is somehow related to the fact that the inner model has a List property, as does my inner model. If I remove the List property, it doesn't generate the duplicate inner model.

I've tried changing the property to Collection<> and ICollection<>, without that change fixing anything.

I've got Scalar running in my project, and the file I can download there has the same behavior. I'm not sure if that is the same code as for Microsoft.Extensions.ApiDescription.Server or not, so not sure that info is relevant.

@CRidge
Copy link

CRidge commented Feb 5, 2025

Looks like a fix is coming on Tuesday:

.NET 9 OpenAPI produces lots of duplicate schemas for the same object

@Berreip
Copy link
Author

Berreip commented Feb 5, 2025

It is somehow related to the fact that the inner model has a List property, as does my inner model. If I remove the List property, it doesn't generate the duplicate inner model.

I've tried changing the property to Collection<> and ICollection<>, without that change fixing anything.

In the reproduction repository that I have provided, changing the List<> for an array does indeed not change the issue.

But is is related to this your are right : the DTO had many more properties but I removed all of them up to the point the issue was still there : That is why there is only a single list remaining.

Looks like a fix is coming on Tuesday:

Nice ! Glad to hear it. :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi
Projects
None yet
Development

No branches or pull requests

3 participants