Thanks to visit codestin.com
Credit goes to github.com

Skip to content

OpenApi generate duplicate schemas #61408

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

Open
1 task done
oPreis-Systecs opened this issue Apr 9, 2025 · 1 comment
Open
1 task done

OpenApi generate duplicate schemas #61408

oPreis-Systecs opened this issue Apr 9, 2025 · 1 comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi

Comments

@oPreis-Systecs
Copy link

oPreis-Systecs commented Apr 9, 2025

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

In OpenApi (Version 9.0.4) schema json there are duplicate schemas if i use arrays.

Expected Behavior

Schemas should not duplicate.

Steps To Reproduce

Controller methods:

[HttpGet("new")]
[Produces("application/json")]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable<EventDto>))]
public async Task<IActionResult> GetAllEventsWithImages()
{
    try
    {
        var events = await _eventLogic.GetAllNewEventsAsync();
        return Ok(events);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error while fetching new events.");
        return StatusCode((int)HttpStatusCode.InternalServerError, InternalServerErrorMessage);
    }
}

[HttpGet("{id:guid}")]
[Produces("application/json")]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(EventDto))]
public async Task<IActionResult> GetEventById(Guid id)
{
    _logger.LogInformation("Fetching event with ID {EventId}", id);
    var eventDto = await _eventLogic.GetEventByIdAsync(id);

    if (eventDto == null)
    {
        _logger.LogWarning("Event with ID {EventId} not found", id);
        return NotFound($"Event with ID {id} not found");
    }

    return Ok(eventDto);
}

Dto:

public class EventDto
{
    public Guid Id { get; set; }
    public Guid? CreatorId { get; set; }
    public string? InputText { get; set; }
    public string? OutputText { get; set; }
    public EventStatus? Status { get; set; }

    public IEnumerable<ImageDto> Images { get; set; } = new List<ImageDto>();
    public IEnumerable<CollectionDto> Collections { get; set; } = new List<CollectionDto>();
}

Generated schema:

{
  "openapi": "3.0.1",
  "info": {
    "title": "Systecs.PortfolioManager.Server | v1",
    "version": "1.0.0"
  },
  "paths": {
    "/api/Event/new": {
      "get": {
        "tags": [
          "Event"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EventDto"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/Event/{id}": {
      "get": {
        "tags": [
          "Event"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventDto2"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CollectionDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "surname": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "date": {
            "type": "string",
            "format": "date-time"
          },
          "imageId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "image": {
            "$ref": "#/components/schemas/ImageDto2"
          },
          "events": {
            "type": "array",
            "items": { }
          }
        }
      },
      "CollectionDto2": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "surname": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "date": {
            "type": "string",
            "format": "date-time"
          },
          "imageId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "image": {
            "$ref": "#/components/schemas/ImageDto2"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventDto"
            }
          }
        }
      },
      "EventDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "creatorId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "inputText": {
            "type": "string",
            "nullable": true
          },
          "outputText": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "$ref": "#/components/schemas/NullableOfEventStatus"
          },
          "images": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImageDto"
            }
          },
          "collections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CollectionDto"
            }
          }
        }
      },
      "EventDto2": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "creatorId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "inputText": {
            "type": "string",
            "nullable": true
          },
          "outputText": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "$ref": "#/components/schemas/NullableOfEventStatus"
          },
          "images": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImageDto"
            }
          },
          "collections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CollectionDto2"
            }
          }
        }
      },
      "ImageDto": {
        "required": [
          "imageData"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "imageData": {
            "type": "string",
            "format": "byte"
          },
          "contentType": {
            "type": "string"
          },
          "type": {
            "$ref": "#/components/schemas/ImageType"
          }
        }
      },
      "ImageDto2": {
        "required": [
          "imageData"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "imageData": {
            "type": "string",
            "format": "byte"
          },
          "contentType": {
            "type": "string"
          },
          "type": {
            "$ref": "#/components/schemas/ImageType"
          }
        },
        "nullable": true
      },
      "ImageType": {
        "type": "integer"
      },
      "NullableOfEventStatus": {
        "type": "integer",
        "nullable": true
      }
    }
  },
  "tags": [
    {
      "name": "Event"
    }
  ]
}

Exceptions (if any)

No response

.NET Version

9.0.200

Anything else?

No response

@ghost ghost added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label Apr 9, 2025
@martincostello martincostello added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi and removed needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically labels Apr 9, 2025
@balukin
Copy link

balukin commented Apr 10, 2025

Probably the same issue here. It was happening prior to 9.0.4, too. I'm using minimal API definitions (WithOpenApi() extension)

public class Response
{
    public Dictionary<string, SomeCustomType>? GroupedResults { get; set; } = new();
    public SomeCustomType? OverallResults { get; set; } 
}

Two schemas are generated #/components/schemas/SomeCustomType and #/components/schemas/SomeCustomType2.

Temporary I can work around this with following configuration but it's too fragile to be a long term solution. Gets API clients to generate in dev loop with no errors, at least.

services.AddOpenApi(options =>
{
    options.AddSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (schema.Properties is not null)
        {
            if (schema.Properties.TryGetValue("overallResults", out var overallResultsRef))
            {
                overallResultsRef.Nullable = true; 
                overallResultsRef.Reference = new OpenApiReference()
                {
                    Id = "SomeCustomType", // forces SomeCustomType instead of SomeCustomType2
                    Type = ReferenceType.Schema,
                };
            }
        }

        return Task.CompletedTask;
    });

});

Strangely enough, resulting JSON doesn't have nullable set to true on overallResults after this modification - I don't know why.

"groupedResults": {
    "type": "object",
    "additionalProperties": {
        "$ref": "#/components/schemas/SomeCustomType"
    },
    "nullable": true
    },
    "overallResults": {
        // would be suffixed with `2` and cause entire new schema to generate without workaround
    "$ref": "#/components/schemas/SomeCustomType"
    },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi
Projects
None yet
Development

No branches or pull requests

3 participants