Skip to content

Recursive data models inside collections yield invalid schema references #59879

Closed
@thorhj

Description

@thorhj

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions