Skip to content

OpenAPI wrong schema generation #64325

@pchalamet

Description

@pchalamet

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

With .net 9, we had Type, Type2, Type3 generated in openapi definition (using Microsoft.Extensions.ApiDescription.Server) - that was buggy but at least tractable.

With .net 10, components are emitted using type name - and not using the type - leading to wrong types being generated if multiple types have same name (not-considering namespace) and completely silently.

Expected Behavior

Components shall be all generated, deduplicated and eventually named with a numbering scheme as before in case of name conflicts.

Steps To Reproduce

OpenApiOneFile.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
    <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

</Project>

Program.cs

namespace SampleApiOneFile.Models {
    public record User {
        public required int Id { get; init; }
        public required string FirstName { get; init; }
        public required string LastName { get; init; }
    }
}

namespace SampleApiOneFile.Models.Patch {
    public record User {
        public string? FirstName { get; init; }
        public string? LastName { get; init; }
    }
}

namespace SampleApiOneFile.Controllers {
    using System.ComponentModel.DataAnnotations;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;

    [ApiController]
    [Route("[controller]")]
    public partial class UserController() : ControllerBase {
        [HttpGet("{id}")]
        public ActionResult<Models.User> Get(int id) {
            return new Models.User {
                Id = id,
                FirstName = $"FirstName {id}",
                LastName = $"LastName {id}"
            };
        }

        [HttpPatch("{id}")]
        [ProducesResponseType<Models.User>(StatusCodes.Status200OK)]
        public ActionResult<Models.User> Update(int id, Models.Patch.User patch) {
            return new Models.User {
                Id = id,
                FirstName = patch.FirstName ?? $"FirstName {id}",
                LastName = patch.LastName ?? $"LastName {id}"
            };
        }

    }    
}

namespace SampleApiOneFile {
    public static class Program {
        public static int Main(string[] args) {
            var builder = WebApplication.CreateBuilder(args);
            builder.Services.AddOpenApi();
            builder.Services.AddControllers();

            var app = builder.Build();
            app.MapControllers();
            app.Run();
            return 0;
        }
    }
}

Generated SampleApiOneFile.json (OpenAPI schema):

{
  "openapi": "3.1.1",
  "info": {
    "title": "SampleApiOneFile | v1",
    "version": "1.0.0"
  },
  "paths": {
    "/User/{id}": {
      "get": {
        "tags": [
          "User"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "User"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/User"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/User"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/User"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "User": {
        "type": "object",
        "properties": {
          "firstName": {
            "type": [
              "null",
              "string"
            ]
          },
          "lastName": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "User"
    }
  ]
}

Look at User component definition: it's plain wrong. We shall have here 2 components:

  • one for Models.User - with no nullable
  • one for Models.Patch.User - with nullable

Exceptions (if any)

No response

.NET Version

10.0.100

Anything else?

macOS 15.7.2

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions