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"