From f7c318c8197a08d12a408113565618f462436a4c Mon Sep 17 00:00:00 2001 From: Peter Aradi Date: Fri, 28 Oct 2022 12:26:43 +0200 Subject: [PATCH 1/2] Do not allow duplicate tag names (OpenAPI object) Addresses issue #5552 --- fastapi/openapi/models.py | 10 +++++++++- fastapi/openapi/utils.py | 5 ++++- tests/test_duplicate_openapi_tags.py | 22 ++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 tests/test_duplicate_openapi_tags.py diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 35aa1672b..40f8aee78 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -2,7 +2,7 @@ from enum import Enum from typing import Any, Callable, Dict, Iterable, List, Optional, Union from fastapi.logger import logger -from pydantic import AnyUrl, BaseModel, Field +from pydantic import AnyUrl, BaseModel, Field, validator try: import email_validator # type: ignore @@ -400,6 +400,14 @@ class OpenAPI(BaseModel): class Config: extra = "allow" + @validator("tags") + def check_tags(cls, tags): # type: ignore + unique_names = set() + assert not any( + t.name in unique_names or unique_names.add(t.name) for t in tags # type: ignore + ), "Tag names must be unique" + return tags + Schema.update_forward_refs() Operation.update_forward_refs() diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 86e15b46d..9fbc8fdff 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -444,5 +444,8 @@ def get_openapi( output["components"] = components output["paths"] = paths if tags: - output["tags"] = tags + # discard tags with non-unique names as it is against the OpenAPI spec + # https://swagger.io/specification/#openapi-object + names = set() + output["tags"] = [t for t in tags if t["name"] not in names and not names.add(t["name"])] # type: ignore return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore diff --git a/tests/test_duplicate_openapi_tags.py b/tests/test_duplicate_openapi_tags.py new file mode 100644 index 000000000..584d21275 --- /dev/null +++ b/tests/test_duplicate_openapi_tags.py @@ -0,0 +1,22 @@ +"""Test case for possible tag duplication at OpenAPI object level""" + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +app = FastAPI( + openapi_tags=[ + {"name": "items", "description": "items1"}, + {"name": "items", "description": "items2"}, + ] +) + +client = TestClient(app) + + +def test_openapi_for_duplicates(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + tag_list = response.json()["tags"] + assert len(tag_list) == 1 + assert tag_list[0]["name"] == "items" + assert tag_list[0]["description"] == "items1" From fc8a4e11fc4b0351b82374f6603601053a7041ce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 12:58:48 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/openapi/models.py | 4 ++-- fastapi/openapi/utils.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index d5eee02d4..e2abca215 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -14,7 +14,6 @@ from pydantic import AnyUrl, BaseModel, Field, validator from typing_extensions import Annotated, Literal, TypedDict from typing_extensions import deprecated as typing_deprecated - try: import email_validator @@ -444,7 +443,8 @@ class OpenAPI(BaseModelWithConfig): def check_tags(cls, tags): # type: ignore unique_names = set() assert not any( - t.name in unique_names or unique_names.add(t.name) for t in tags # type: ignore + t.name in unique_names or unique_names.add(t.name) + for t in tags # type: ignore ), "Tag names must be unique" return tags diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index e51cafb56..5f533b8c2 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -568,5 +568,7 @@ def get_openapi( # discard tags with non-unique names as it is against the OpenAPI spec # https://swagger.io/specification/#openapi-object names = set() - output["tags"] = [t for t in tags if t["name"] not in names and not names.add(t["name"])] # type: ignore + output["tags"] = [ + t for t in tags if t["name"] not in names and not names.add(t["name"]) + ] # type: ignore return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore