Skip to content

An invalid reference is generated for a object that contains two properties of type List<List<string>> (both of the same type). #60381

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
mahald opened this issue Feb 14, 2025 · 7 comments
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi

Comments

@mahald
Copy link

mahald commented Feb 14, 2025

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Describe the bug

For the following Type:

  using System.ComponentModel;
  
  namespace ManagementApiServer.Models;
  public record DynamicTableDataPatch()
  {
      [Description("List of records to delete. Only the first column (primary key) is considered for deletion.")]
      public List<List<string>> Delete { get; set; } = [];
  
      [Description("List of records to add or update.")]
      public List<List<string>> AddOrUpdate { get; set; } = [];
  
  }

a wrong (at least neither swagger ui nor scalar can handle it) reference is created for the second occurance of List<List>

Image

OpenApi File To Reproduce

{
  "openapi": "3.0.1",
  "info": {
    "title": "ManagementApiServer | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://localhost:7093"
    },
    {
      "url": "http://localhost:5192"
    }
  ],
  "paths": {
    "/managementApi/v1/dynamicTables/{id}": {
      "patch": {
        "tags": [
          "ManagementApiServer"
        ],
        "description": "Performs an atomic update operation on the specified records, ensuring all changes are applied together or not at all.",
        "operationId": "PatchDynamicTable",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DynamicTableDataPatch"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "No Content"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          },
          "500": {
            "description": "Internal Server Error"
          }
        }
      },
      "get": {
        "tags": [
          "ManagementApiServer"
        ],
        "operationId": "GetDynamicTable",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          },
          "500": {
            "description": "Internal Server Error"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/managementApi/v1/dynamicTables": {
      "get": {
        "tags": [
          "ManagementApiServer"
        ],
        "operationId": "GetDynamicTables",
        "responses": {
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          },
          "500": {
            "description": "Internal Server Error"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/DynamicTable"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "DynamicTable": {
        "required": [
          "name",
          "dynamicTableDefinitionId",
          "created",
          "modifed"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "dynamicTableDefinitionId": {
            "type": "string",
            "format": "uuid"
          },
          "description": {
            "type": "string"
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "modifed": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DynamicTableDataPatch": {
        "type": "object",
        "properties": {
          "delete": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "List of records to delete. Only the first column (primary key) is considered for deletion."
          },
          "addOrUpdate": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/#/properties/delete/items"
            },
            "description": "List of records to add or update."
          }
        }
      }
    },
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "description": "Custom authentication header",
        "name": "Api-Key",
        "in": "header"
      }
    }
  },
  "tags": [
    {
      "name": "ManagementApiServer"
    }
  ]
}

Expected Behavior

Expected behavior
The reference seems invalid.
At least neither scalar nor swagger-ui can handle it.
swagger ui reports a invalid reference.

if i change the second # to the actual Name it works:
Image

Steps To Reproduce

Using a class with List<> seems to result in this wrong reference:

app.MapPatch("/test1234", async ([FromBody] DynamicTableDataPatch records) =>
{
    // ....
    return Results.NoContent();
})
.Produces(204)
.Produces(403)
.Produces(404)
.Produces(500)
.Accepts<DynamicTableDataPatch>(contentType: "application/json")
.WithOpenApi();
  using System.ComponentModel;
  
  namespace ManagementApiServer.Models;
  public record DynamicTableDataPatch()
  {
      [Description("List of records to delete. Only the first column (primary key) is considered for deletion.")]
      public List<List<string>> Delete { get; set; } = [];
  
      [Description("List of records to add or update.")]
      public List<List<string>> AddOrUpdate { get; set; } = [];
  
  }

Exceptions (if any)

No response

.NET Version

9.0.200

Anything else?

9.0.2 of the nuget package

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label Feb 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 Feb 14, 2025
@dc-davidlazauskas
Copy link

+1 also hitting this issue with multiple models where the type is a class rather than a string. It would be nice if the reference (#/components/schemas/#/properties/delete/items) was the same as the first reference (string).

@mahald
Copy link
Author

mahald commented Feb 17, 2025

+1 also hitting this issue with multiple models where the type is a class rather than a string. It would be nice if the reference (#/components/schemas/#/properties/delete/items) was the same as the first reference (string).

emergency fixing it for the time being:

var app = builder.Build();
app.MapOpenApi("/managementApi/v1/openapiErr.json");
app.MapScalarApiReference("/managementApi/v1/scalar", options => {  options.OpenApiRoutePattern = "/managementApi/v1/openapi.json"; });
//app.UseHttpsRedirection();

app.MapGet("/managementApi/v1/openapi.json", async (HttpRequest request) =>
{
    var openApiUrl = $"{request.Scheme}://{request.Host}/managementApi/v1/openapiErr.json";
    var response = await Global.HttpClient.GetAsync(openApiUrl);
    response.EnsureSuccessStatusCode();
    var jsonContent = await response.Content.ReadAsStringAsync();
    jsonContent = jsonContent.Replace("#/properties/delete/items", "DynamicTableDataPatch/properties/delete/items");
    var jsonObject = JsonNode.Parse(jsonContent)?.AsObject();
    return Results.Json(jsonObject, contentType: "application/json");
})
.ExcludeFromDescription()
.Produces<JsonObject>(200, contentType: "application/json");

/// .....


public static class Global
{
    public static HttpClient HttpClient = new HttpClient();
}

@Urganot
Copy link

Urganot commented Feb 18, 2025

I am also experiencing this error. At least it generates the same wrong stuff in the openapi.json
In my case though I cant find a class that has the same list type twice. For me it happens with a complex type, not with a base type

@marcominerva
Copy link
Contributor

This is the same issue of #60012.

@captainsafia
Copy link
Member

Hey all! Thanks for reporting this issue.

The root cause here (as with many of the other schema related issues) is that the schema generated by STJ's GetSchemaAsJsonNode API uses relative references if it encounters a type that has already been seen in the type hierarchy before. In the example above, the AddOrUpdate property has the same type as the Delete property so STJ "reuses" the schema it generated for the first property via a relative reference.

For these scenarios, we need to explode out relative references before passing them into the OpenApiDocument, which doesn't process relative references well in v1.6 (see microsoft/OpenAPI.NET#2062).

This will be resolved for 9.0.x via #60410. For .NET 10 and beyond, we'll be relying on a change in the underlying Microsoft.OpenApi library.

@captainsafia
Copy link
Member

The bug fix for this has been merged into the servicing branch and will be included in 9.0.4.

@daniatic
Copy link

@captainsafia is there a prerelease I can use? Were not in production yet, so that would be fine. Or can I use a .net 10 prerelease in the meantime?

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

7 participants