Skip to content

OpenAPI document generation does not generate correct refs for self-referencing collection properties #58006

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
null-d3v opened this issue Sep 21, 2024 · 5 comments
Assignees
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue.
Milestone

Comments

@null-d3v
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Microsoft.AspNetCore.OpenApi will generate an invalid JSON schema when a parameter has a self-reference collection property. This does not seem to occur if the self reference is not a collection.

Using the following as a parameter:

public class Criteria
{
    public IEnumerable<Criteria> SubCriteria { get; set; } = [ ];
}

[FromBody] Criteria criteria

Will result in the JSON schema:

  "components": {
    "schemas": {
      "Criteria": {
        "type": "object",
        "properties": {
          "subCriteria": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/#"
            }
          }
        }
      }
    }
  },

Which has an invalid reference:

Resolver error at components.schemas.Criteria.properties.subCriteria.items.$ref
Could not resolve reference: Could not resolve pointer: /components/schemas/ does not exist in document

This occurs with both minimal APIs and controllers.

Expected Behavior

I would expect the subCriteria items reference to correctly point at #/components/schemas/Criteria.

Steps To Reproduce

Here is a complete self-contained Program.cs which will generate the invalid OpenAPI document:

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi().CacheOutput();

app.MapPost("/item/search", ([FromBody] Criteria criteria) =>
{
    var items = Enumerable
        .Range(1, 5)
        .Select(index =>
            new Item
            {
                Name = index.ToString(),
            })
        .ToArray();
    return items;
})
.WithName("SearchItems")
.WithOpenApi();

app.Run();

public class Item
{
    public string Name { get; set; } = default!;
}

public class Criteria
{
    public IEnumerable<Criteria> SubCriteria { get; set; } = [ ];
}

And the OpenAPI document generated at /openapi/v1.json:

{
  "openapi": "3.0.1",
  "info": {
    "title": "Sample | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://localhost:5129"
    }
  ],
  "paths": {
    "/item/search": {
      "post": {
        "tags": [
          "Sample"
        ],
        "operationId": "SearchItems",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Criteria"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Criteria": {
        "type": "object",
        "properties": {
          "subCriteria": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/#"
            }
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Sample"
    }
  ]
}

Exceptions (if any)

No response

.NET Version

9.0.100-rc.1.24452.12

Anything else?

No response

@ghost ghost added the old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Sep 21, 2024
@captainsafia
Copy link
Member

@null-d3v Thanks for reporting this issue!

I'm hoping to drop a fix for this in the upcoming .NET 9 GA release. Once the change is merged, you can try out the fix in one of our nightly releases of the package. If you're interested in learning how to do that, let me know and I can share instructions.

@captainsafia captainsafia self-assigned this Sep 23, 2024
@captainsafia captainsafia added this to the 9.0.0 milestone Sep 23, 2024
@captainsafia captainsafia added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Sep 23, 2024
@null-d3v
Copy link
Author

@captainsafia Thanks for the prompt reply!! I haven't had to pull from the nightly feed yet, but I've worked out the setup. Very enthused about this in .NET 9! We have a lot of document generation with NSwag that we're anticipating to change.

wtgodbe pushed a commit that referenced this issue Sep 25, 2024
* Fix JsonUnmappedMemberHandling attribute handling to close #57981

* Fix enum handling for MVC actions to close #57979

* Fix self-referential schema handling to close #58006

* Fix concurrent request handling for OpenAPI documents (#57972)

* fix: Allow concurrent requests

* test: Update test

* test: Use Parallel.ForEachAsync

* feat: Use valueFactory overload

* feat: Pass valueFactory directly

* Harden self-referencing schema ID check

---------

Co-authored-by: Justin Lampe <[email protected]>
captainsafia added a commit that referenced this issue Sep 26, 2024
* Fix JsonUnmappedMemberHandling attribute handling to close #57981

* Fix enum handling for MVC actions to close #57979

* Fix self-referential schema handling to close #58006

* Fix concurrent request handling for OpenAPI documents (#57972)

* fix: Allow concurrent requests

* test: Update test

* test: Use Parallel.ForEachAsync

* feat: Use valueFactory overload

* feat: Pass valueFactory directly

* Harden self-referencing schema ID check

---------

Co-authored-by: Justin Lampe <[email protected]>
@captainsafia
Copy link
Member

@null-d3v The fix for this has landed in nightly package version 9.0.0-rtm.24476.2.

You'll need to use the following PackageReference:

<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0-rtm.24476.2" />

And make sure that you have a reference to the nightly dotnet9 feed in your nuget.config file:

<add key="dotnet9" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />

Can you verify the fix on your end?

@captainsafia captainsafia added the Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. label Sep 26, 2024
@null-d3v
Copy link
Author

null-d3v commented Oct 7, 2024

Sorry for the late reply, this is definitely fixed in 9.0.0-rtm.24476.2

captainsafia added a commit that referenced this issue Dec 31, 2024
… (#58096)

* Fix JsonUnmappedMemberHandling attribute handling to close #57981

* Fix enum handling for MVC actions to close #57979

* Fix self-referential schema handling to close #58006

* Fix concurrent request handling for OpenAPI documents (#57972)

* fix: Allow concurrent requests

* test: Update test

* test: Use Parallel.ForEachAsync

* feat: Use valueFactory overload

* feat: Pass valueFactory directly

* Harden self-referencing schema ID check

---------

Co-authored-by: Justin Lampe <[email protected]>
@domints
Copy link

domints commented Jan 27, 2025

EDIT: I found it's being tracked under #60012

I found another two cases where it falls apart with those # paths:

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi().CacheOutput();

app.MapGet("/item/search", () =>
{
    var criteria = new Criteria
    {
        Id = 0,
        SubCriteria = Enumerable
            .Range(1, 5)
            .Select(id => new Criteria { Id = id })
            .ToList()
    };
    return new OperationResult<Criteria> { Data = criteria };
})
.WithName("SearchItems")
.WithOpenApi();

app.MapGet("/item/search2", () =>
{
    var table = new Table
    {
        Title = "example table",
        Rows = Enumerable
            .Range(1, 5)
            .Select(id => new Row { Name = id.ToString() })
            .ToList()
    };
    return new OperationResult<Table> { Data = table };
})
.WithName("SearchItems2")
.WithOpenApi();

app.Run();

public class OperationResult<T>
{
    public List<string> Errors { get; set; } = new List<string>();
    public T? Data { get; set; }
}

public class Table
{
    public required string Title { get; set; }
    public List<Table> SubTables { get; set; } = new List<Table>();
    public List<Row> Rows { get; set; } = new List<Row>();
}

public class Row
{
    public string Name { get; set; } = default!;
}

public class Criteria
{
    public int Id { get; set; }
    public List<Criteria> SubCriteria { get; set; } = new List<Criteria> ();
}

It's on version 9.0.1 of package, what is weird it fails in different way than it fails on my real life app.

Resulting json:

{
  "openapi": "3.0.1",
  "info": {
    "title": "aspnetcore-issue-openapi-self-reference | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://localhost:5008"
    }
  ],
  "paths": {
    "/item/search": {
      "get": {
        "tags": [
          "aspnetcore-issue-openapi-self-reference"
        ],
        "operationId": "SearchItems",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OperationResultOfCriteria"
                }
              }
            }
          }
        }
      }
    },
    "/item/search2": {
      "get": {
        "tags": [
          "aspnetcore-issue-openapi-self-reference"
        ],
        "operationId": "SearchItems2",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OperationResultOfTable"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Criteria": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "subCriteria": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Criteria2"
            }
          }
        },
        "nullable": true
      },
      "Criteria2": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "subCriteria": {
            "$ref": "#/components/schemas/#/properties/data/properties/subCriteria"
          }
        }
      },
      "OperationResultOfCriteria": {
        "type": "object",
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "data": {
            "$ref": "#/components/schemas/Criteria"
          }
        }
      },
      "OperationResultOfTable": {
        "type": "object",
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "data": {
            "$ref": "#/components/schemas/Table"
          }
        }
      },
      "Row": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          }
        }
      },
      "Table": {
        "required": [
          "title"
        ],
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "subTables": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Table2"
            }
          },
          "rows": {
            "$ref": "#/components/schemas/#/properties/data/properties/subTables/items/properties/rows"
          }
        },
        "nullable": true
      },
      "Table2": {
        "required": [
          "title"
        ],
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "subTables": {
            "$ref": "#/components/schemas/#/properties/data/properties/subTables"
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Row"
            }
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "aspnetcore-issue-openapi-self-reference"
    }
  ]
}

I'm pretty sure generics are at fault here, when you remove OperationResult from chain it all does well.

Fun fact: it doesn't generate those Table2/Criteria2 definitions in my code, available on this repo: https://github.com/domints/Invee/tree/c98f8b5aab6fba69822d01b5732d7ac2b9ae80b4 but it does break with those weird refs.

This definition from my project is especially interesting, since Children, yes, should be same type, but Items is not:

"Category": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "parentId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "nullable": true
          },
          "parent": {
            "$ref": "#/components/schemas/Category"
          },
          "children": {
            "$ref": "#/components/schemas/#/properties/data/properties/parent/properties/children"
          },
          "items": {
            "$ref": "#/components/schemas/#/properties/data/properties/parent/properties/children/items/properties/items"
          }
        },
        "nullable": true
      }

It also doesn't do it always, but I didn't figure out yet why it does and does not.

Thanks for any help.

captainsafia added a commit that referenced this issue Feb 11, 2025
… (#58096)

* Fix JsonUnmappedMemberHandling attribute handling to close #57981

* Fix enum handling for MVC actions to close #57979

* Fix self-referential schema handling to close #58006

* Fix concurrent request handling for OpenAPI documents (#57972)

* fix: Allow concurrent requests

* test: Update test

* test: Use Parallel.ForEachAsync

* feat: Use valueFactory overload

* feat: Pass valueFactory directly

* Harden self-referencing schema ID check

---------

Co-authored-by: Justin Lampe <[email protected]>
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 area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue.
Projects
None yet
Development

No branches or pull requests

4 participants