Skip to content

Microsoft.AspNetCore.OpenAPI duplicating schemas when returning generic wrappers and reusing model #62508

Open
@v-stewartp

Description

@v-stewartp

Summary

Using Microsoft.AspNetCore.OpenAPI version 9.0.6 creates duplicate schema definitions if the following conditions are met:

  1. Model: ApiResponse<T>: A generic wrapper to be used as endpoint response types.
  2. ModelA: A simple model
  3. ModelB: Another simple model that has ModelA as a property
  4. Controller endpoints:
    a. EndpointA: Return type of ApiResponse<ModelA>
    a. EndpointB: Return type of ApiResponse<ModelB>

When Generating the OpenAPI .json definition, ModelA will have 2 schema definitions:

  1. ModelA: This represents the return type from 4.a
  2. ModelA2: This represents the return type from 4.b (Note: ModelB will have a ref to #/components/schemas/ModelA)

Motivation and goals

As of net9.0, support for Swagger (NSwag) is deprecated in favour of OpenAPI. We are rewriting our API documentation layer to use OpenAPI so that we can transition to LTS version net10.0. However, using OpenAPI has the following effect:

Using the OpenAPI.json definition described above, our team generates an API Client for service consumers. As a result:

  1. There a hundreds more lines of .json and API Client .cs code.
  2. Previously, the API Client (generated from Swagger 2.0), would aggressively de-duplicate the schemas to ensure 1 schema definition for a singular DTO / model.

The extra Schemas are a breaking change as our API consumers will now need to react to:

  1. Changing from ModelA to ModelAx
  2. It is programmatically cumbersome to determine which variant of schema to use in code.

Minimum Repro steps:

.csproj:

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

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>


  <ItemGroup>
    <PackageReference Include="Microsoft.OpenApi" />
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" />
    <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" />
  </ItemGroup>

</Project>

Program.cs:


using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();

builder.Services.AddOpenApi();

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Models / DTOS:

public class ApiResponse<T>
{
    public T? Result { get; set; }
}

public class SingleItem
{
    public string Name { get; set; } = string.Empty;
}

public class CollectionItems
{
    public IList<SingleItem> Items { get; set; } = new List<SingleItem>();
}

Controller:


[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
    [HttpGet]
    public ActionResult<ApiResponse<SingleItem>> GetSingleItem()
    {
        return new ApiResponse<SingleItem>
        {
            Result = new SingleItem { Name = "Test" }
        };
    }

    [HttpGet]
    public ActionResult<ApiResponse<CollectionItems>> GetCollectionItems()
    {
        return new ApiResponse<CollectionItems>
        {
            Result = new CollectionItems()
        };
    }
}

This results in:


{
  "openapi": "3.0.4",
  "info": {
    "title": "OpenApi.DeduplicateSchemas | v1",
    "version": "1.0.0"
  },
  "paths": {
    "/api/Test": {
      "get": {
        "tags": [
          "Test"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCollectionItems"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCollectionItems"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponseOfCollectionItems"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ApiResponseOfCollectionItems": {
        "type": "object",
        "properties": {
          "result": {
            "$ref": "#/components/schemas/CollectionItems"
          }
        }
      },
      "ApiResponseOfSingleItem": {
        "type": "object",
        "properties": {
          "result": {
            "$ref": "#/components/schemas/SingleItem"
          }
        }
      },
      "CollectionItems": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SingleItem2"
            }
          }
        },
        "nullable": true
      },
      "SingleItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          }
        },
        "nullable": true
      },
      "SingleItem2": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Test"
    }
  ]
}

Similar issues:

  1. Fix self-referential schema handling for collection-based types #60339
  2. .NET 9 OpenAPI produces lots of duplicate schemas for the same object #58968 (comment) - This post indicates that net10.0 will resolve the scenario in the issue and that the fix will be back ported to Microsoft.AspNetCore.OpenAPI 9. I have upgraded to Microsoft.AspNetCore.OpenAPI 9.0.6 (The latest version) and my issue remains.

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