diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index ed07b40f5..e2abca215 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -10,7 +10,7 @@ from fastapi._compat import ( with_info_plain_validator_function, ) from fastapi.logger import logger -from pydantic import AnyUrl, BaseModel, Field +from pydantic import AnyUrl, BaseModel, Field, validator from typing_extensions import Annotated, Literal, TypedDict from typing_extensions import deprecated as typing_deprecated @@ -439,6 +439,15 @@ class OpenAPI(BaseModelWithConfig): tags: Optional[List[Tag]] = None externalDocs: Optional[ExternalDocumentation] = None + @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 + _model_rebuild(Schema) _model_rebuild(Operation) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 808646cc2..5f533b8c2 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -565,5 +565,10 @@ def get_openapi( if webhook_paths: output["webhooks"] = webhook_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"