Skip to content

Recursive data models inside collections yield invalid schema references #59879

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

Closed
1 task done
thorhj opened this issue Jan 14, 2025 · 3 comments
Closed
1 task done
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi ✔️ Resolution: Duplicate Resolved as a duplicate of another issue Status: Resolved

Comments

@thorhj
Copy link

thorhj commented Jan 14, 2025

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

The new Microsoft.AspNetCore.OpenApi package yields invalid schema references when using recursive data models inside collections.

Here is an example of a recursive model Tree because it contains a list of its own type in the property Children:

record Tree
{
    public required int Value { get; init; }
    public required IReadOnlyCollection<Tree> Children { get; init; }
}

Return this through an endpoint, and the resulting schema is correct.

app.MapGet("/tree",
    () => new Tree
    {
        Value = 1,
        Children = []
    });
// (partial) output:
"components": {
    "schemas": {
      "Tree": {
        "required": [
          "value",
          "children"
        ],
        "type": "object",
        "properties": {
          "value": {
            "type": "integer",
            "format": "int32"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tree"
            }
          }
        }
      }
    }
  }

Notice here how the components.schemas.Tree.properties.children.items."$ref" points back to Tree - so far so good. The trouble comes when I add the recursive model in a collection in another model:

record Trees
{
    public required List<Tree> List { get; init; }
}

app.MapGet("/trees",
    () => new Trees { List = [] });

Two problems occur here. First of all, the Tree model inside Tree.List gets its own schema (called Tree2 in my case). The other problem is, the new components.schemas.Tree2.properties.children.items."$ref" looks to refer back to the list of the enclosing Trees model:

  "components": {
    "schemas": {
      "Tree": {
        // ...
      },
      "Tree2": {    // <--- This shouldn't exist, could just re-use Tree
        "required": [
          "value",
          "children"
        ],
        "type": "object",
        "properties": {
          "value": {
            "type": "integer",
            "format": "int32"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/#/properties/list/items"    // <--- This schema reference is incorrect
            }
          }
        }
      },
      "Trees": {
        "required": [
          "list"
        ],
        "type": "object",
        "properties": {
          "list": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tree2"
            }
          }
        }
      }
    }
  }
}

If I only had the /trees/ endpoint, the Tree schema would be necessary to generate of course. However, here it already exists. The schema reference is incorrect and should just point to Tree2 or ideally Tree. I am not well-versed in the OpenAPI specification, so I determine it's "incorrect" because the Spectral Linter calls it out and Kiota is unable to parse this schema reference.

The same thing happens if I put Tree inside a Dictionary. It generates this type of schema reference:

"$ref": "#/components/schemas/#/properties/lookup/additionalProperties/items"

Expected Behavior

Generate the schema without duplicate Tree definitions and with correct schema references:

  "components": {
    "schemas": {
      "Tree": { 
        "required": [
          "value",
          "children"
        ],
        "type": "object",
        "properties": {
          "value": {
            "type": "integer",
            "format": "int32"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tree"
            }
          }
        }
      },
      "Trees": {
        "required": [
          "list"
        ],
        "type": "object",
        "properties": {
          "list": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tree"
            }
          }
        }
      }
    }
  }
}

Steps To Reproduce

I have created a minimal API showing the behavior:
https://gist.github.com/thorhj/933f07533ff1c8206ed7ebefd4d91c6a

In it I have included an endpoint returning just the recursive model Tree (valid), the recursive model inside a List<Tree> property (invalid) and the recursive data model inside a Dictionary<int, Tree> (invalid).

Exceptions (if any)

No response

.NET Version

9.0.101

Anything else?

> dotnet --info
.NET SDK:
 Version:           9.0.101
 Commit:            eedb237549
 Workload version:  9.0.100-manifests.3068a692
 MSBuild version:   17.12.12+1cce77968

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.22631
 OS Platform: Windows
 RID:         win-x64
 Base Path:   C:\Program Files\dotnet\sdk\9.0.101\

.NET workloads installed:
There are no installed workloads to display.
Configured to use loose manifests when installing new manifests.

Host:
  Version:      9.0.0
  Architecture: x64
  Commit:       9d5a6a9aa4

.NET SDKs installed:
  8.0.206 [C:\Program Files\dotnet\sdk]
  8.0.404 [C:\Program Files\dotnet\sdk]
  9.0.101 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 8.0.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 8.0.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 9.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.36 [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 8.0.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 9.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 6.0.36 [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 8.0.11 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 9.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
  None

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download
@ghost ghost added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label Jan 14, 2025
@martincostello martincostello added feature-openapi area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically labels Jan 14, 2025
@captainsafia
Copy link
Member

@thorhj Thanks for filing this issue!

I believe what you've filed is a dupe of the recently resolved #58968.

@captainsafia captainsafia added the ✔️ Resolution: Duplicate Resolved as a duplicate of another issue label Jan 15, 2025
@thorhj
Copy link
Author

thorhj commented Jan 15, 2025

I didn't realize that issue also touched on the recursive model issue. After reading the discussion I believe my issue is indeed a duplicate of #58968. Sorry about that 🥲

Copy link
Contributor

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.

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 ✔️ Resolution: Duplicate Resolved as a duplicate of another issue Status: Resolved
Projects
None yet
Development

No branches or pull requests

3 participants