Skip to content

Recursive data models inside collections yield invalid schema references #59879

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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