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

Closed

Description

@mahald

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

Activity

  1. ghost added
    needs-area-labelUsed by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically
    on Feb 14, 2025
  2. added
    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etc
    and removed
    needs-area-labelUsed by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically
    on Feb 14, 2025
  3. dc-davidlazauskas commented on Feb 14, 2025

    @dc-davidlazauskas

    +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).

  4. mahald commented on Feb 17, 2025

    @mahald
    Author

    +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();
    }
    
  5. Urganot commented on Feb 18, 2025

    @Urganot

    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

  6. marcominerva commented on Feb 18, 2025

    @marcominerva
    Contributor

    This is the same issue of #60012.

  7. captainsafia commented on Mar 5, 2025

    @captainsafia
    Contributor

    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.

  8. captainsafia commented on Mar 11, 2025

    @captainsafia
    Contributor

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

  9. daniatic commented on Mar 27, 2025

    @daniatic

    @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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcfeature-openapi

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions