From 571ff009b66d5282c3894b47cd1548a60ca0a901 Mon Sep 17 00:00:00 2001 From: Nisar k Date: Mon, 18 May 2026 10:58:08 +0000 Subject: [PATCH] fix: generate unique operation ids for multi-method routes --- fastapi/openapi/utils.py | 11 ++++++++- fastapi/utils.py | 10 +++++--- tests/test_generate_unique_id_function.py | 30 +++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 1c7a17c4ca..d0bb2c06d0 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -31,6 +31,7 @@ from fastapi.responses import Response from fastapi.sse import _SSE_EVENT_SCHEMA from fastapi.types import ModelNameMap from fastapi.utils import ( + _generate_unique_id_for_method, deep_dict_update, generate_operation_id_for_path, is_body_allowed_for_status_code, @@ -242,7 +243,15 @@ def get_openapi_operation_metadata( operation["summary"] = generate_operation_summary(route=route, method=method) if route.description: operation["description"] = route.description - operation_id = route.operation_id or route.unique_id + if ( + route.operation_id is None + and isinstance(route.generate_unique_id_function, DefaultPlaceholder) + and route.methods + and len(route.methods) > 1 + ): + operation_id = _generate_unique_id_for_method(route, method=method) + else: + operation_id = route.operation_id or route.unique_id if operation_id in operation_ids: endpoint_name = getattr(route.endpoint, "__name__", "") message = f"Duplicate Operation ID {operation_id} for function {endpoint_name}" diff --git a/fastapi/utils.py b/fastapi/utils.py index 12eaa2bf08..250d4b1583 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -92,14 +92,18 @@ def generate_operation_id_for_path( return operation_id -def generate_unique_id(route: "APIRoute") -> str: +def _generate_unique_id_for_method(route: "APIRoute", method: str) -> str: operation_id = f"{route.name}{route.path_format}" operation_id = re.sub(r"\W", "_", operation_id) - assert route.methods - operation_id = f"{operation_id}_{list(route.methods)[0].lower()}" + operation_id = f"{operation_id}_{method.lower()}" return operation_id +def generate_unique_id(route: "APIRoute") -> str: + assert route.methods + return _generate_unique_id_for_method(route, method=list(route.methods)[0]) + + def deep_dict_update(main_dict: dict[Any, Any], update_dict: dict[Any, Any]) -> None: for key, value in update_dict.items(): if ( diff --git a/tests/test_generate_unique_id_function.py b/tests/test_generate_unique_id_function.py index c56e6d5794..10efcab68d 100644 --- a/tests/test_generate_unique_id_function.py +++ b/tests/test_generate_unique_id_function.py @@ -1697,3 +1697,33 @@ def test_warn_duplicate_operation_id(): ] assert len(duplicate_warnings) > 0 assert "Duplicate Operation ID" in str(duplicate_warnings[0].message) + + +def test_multi_method_route_unique_operation_ids(): + app = FastAPI() + + def clear(): + return {"ok": True} # pragma: nocover + + app.add_api_route("/clear", clear, methods=["POST", "DELETE"]) + + client = TestClient(app) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + + duplicate_warnings = [ + warning + for warning in w + if issubclass(warning.category, UserWarning) + and "Duplicate Operation ID" in str(warning.message) + ] + assert duplicate_warnings == [] + openapi_schema = response.json() + assert openapi_schema["paths"]["/clear"]["post"]["operationId"] == ( + "clear_clear_post" + ) + assert openapi_schema["paths"]["/clear"]["delete"]["operationId"] == ( + "clear_clear_delete" + )