From a8f189ad7a9cbb905785d1b58086d9a338812ee2 Mon Sep 17 00:00:00 2001 From: Anandesh Sharma Date: Fri, 27 Feb 2026 04:02:47 +0530 Subject: [PATCH] Fix duplicate OperationID when adding route with multiple methods When a route handles multiple HTTP methods (e.g. methods=["POST", "DELETE"]), the operation ID was generated once from the first method in the set, causing duplicate operation IDs in the OpenAPI schema and triggering warnings. Now, for routes with multiple methods, the current method is appended to the unique_id to ensure each method gets a distinct operation ID. Closes #13175 --- fastapi/openapi/utils.py | 9 ++- ...duplicate_operation_id_multiple_methods.py | 71 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/test_duplicate_operation_id_multiple_methods.py diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 812003aee3..c7c92350cc 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -242,7 +242,14 @@ 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: + operation_id = route.operation_id + elif route.methods is not None and len(route.methods) > 1: + # When a route handles multiple HTTP methods, append the current + # method to create unique operation IDs per method + operation_id = f"{route.unique_id}_{method.lower()}" + else: + operation_id = route.unique_id if operation_id in operation_ids: message = ( f"Duplicate Operation ID {operation_id} for function " diff --git a/tests/test_duplicate_operation_id_multiple_methods.py b/tests/test_duplicate_operation_id_multiple_methods.py new file mode 100644 index 0000000000..53b103759d --- /dev/null +++ b/tests/test_duplicate_operation_id_multiple_methods.py @@ -0,0 +1,71 @@ +import warnings + +from fastapi import FastAPI +from fastapi.testclient import TestClient + + +def test_no_duplicate_operation_id_with_multiple_methods(): + """Ensure routes with multiple methods get unique operation IDs.""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + app = FastAPI() + + def handler(): + return {"status": "ok"} # pragma: nocover + + app.add_api_route("/items", handler, methods=["POST", "DELETE"]) + + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + schema = response.json() + + post_op = schema["paths"]["/items"]["post"] + delete_op = schema["paths"]["/items"]["delete"] + + assert post_op["operationId"] != delete_op["operationId"] + assert "post" in post_op["operationId"] + assert "delete" in delete_op["operationId"] + + dup_warnings = [x for x in w if "Duplicate" in str(x.message)] + assert len(dup_warnings) == 0 + + +def test_single_method_route_operation_id_unchanged(): + """Ensure single-method routes still generate the same operation IDs.""" + app = FastAPI() + + @app.get("/items") + def get_items(): + return [] # pragma: nocover + + client = TestClient(app) + response = client.get("/openapi.json") + schema = response.json() + + get_op = schema["paths"]["/items"]["get"] + assert get_op["operationId"] == "get_items_items_get" + + +def test_three_methods_unique_operation_ids(): + """Ensure routes with three methods all get unique operation IDs.""" + app = FastAPI() + + def handler(): + return {"status": "ok"} # pragma: nocover + + app.add_api_route("/items", handler, methods=["GET", "POST", "DELETE"]) + + client = TestClient(app) + response = client.get("/openapi.json") + schema = response.json() + + get_op = schema["paths"]["/items"]["get"] + post_op = schema["paths"]["/items"]["post"] + delete_op = schema["paths"]["/items"]["delete"] + + assert "get" in get_op["operationId"] + assert "post" in post_op["operationId"] + assert "delete" in delete_op["operationId"] + assert len({get_op["operationId"], post_op["operationId"], delete_op["operationId"]}) == 3