]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Fix OpenAPI duplication of `anyOf` refs for app-level responses with specified...
authorDJ Melisso <DJMcoder@users.noreply.github.com>
Wed, 4 Feb 2026 13:23:08 +0000 (05:23 -0800)
committerGitHub <noreply@github.com>
Wed, 4 Feb 2026 13:23:08 +0000 (14:23 +0100)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com>
fastapi/openapi/utils.py
tests/test_additional_responses_union_duplicate_anyof.py [new file with mode: 0644]

index 75ff261025213c858040f0ee626f2ded7bc808bd..d56027b50015ddf45a81896497489173883df93e 100644 (file)
@@ -1,3 +1,4 @@
+import copy
 import http.client
 import inspect
 import warnings
@@ -377,7 +378,7 @@ def get_openapi_path(
                     additional_status_code,
                     additional_response,
                 ) in route.responses.items():
-                    process_response = additional_response.copy()
+                    process_response = copy.deepcopy(additional_response)
                     process_response.pop("model", None)
                     status_code_key = str(additional_status_code).upper()
                     if status_code_key == "DEFAULT":
diff --git a/tests/test_additional_responses_union_duplicate_anyof.py b/tests/test_additional_responses_union_duplicate_anyof.py
new file mode 100644 (file)
index 0000000..f5d987c
--- /dev/null
@@ -0,0 +1,123 @@
+"""
+Regression test: Ensure app-level responses with Union models and content/examples
+don't accumulate duplicate $ref entries in anyOf arrays.
+See https://github.com/fastapi/fastapi/pull/14463
+"""
+
+from typing import Union
+
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+
+class ModelA(BaseModel):
+    a: str
+
+
+class ModelB(BaseModel):
+    b: str
+
+
+app = FastAPI(
+    responses={
+        500: {
+            "model": Union[ModelA, ModelB],
+            "content": {"application/json": {"examples": {"Case A": {"value": "a"}}}},
+        }
+    }
+)
+
+
+@app.get("/route1")
+async def route1():
+    pass  # pragma: no cover
+
+
+@app.get("/route2")
+async def route2():
+    pass  # pragma: no cover
+
+
+client = TestClient(app)
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/route1": {
+                "get": {
+                    "summary": "Route1",
+                    "operationId": "route1_route1_get",
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "500": {
+                            "description": "Internal Server Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "anyOf": [
+                                            {"$ref": "#/components/schemas/ModelA"},
+                                            {"$ref": "#/components/schemas/ModelB"},
+                                        ],
+                                        "title": "Response 500 Route1 Route1 Get",
+                                    },
+                                    "examples": {"Case A": {"value": "a"}},
+                                }
+                            },
+                        },
+                    },
+                }
+            },
+            "/route2": {
+                "get": {
+                    "summary": "Route2",
+                    "operationId": "route2_route2_get",
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "500": {
+                            "description": "Internal Server Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "anyOf": [
+                                            {"$ref": "#/components/schemas/ModelA"},
+                                            {"$ref": "#/components/schemas/ModelB"},
+                                        ],
+                                        "title": "Response 500 Route2 Route2 Get",
+                                    },
+                                    "examples": {"Case A": {"value": "a"}},
+                                }
+                            },
+                        },
+                    },
+                }
+            },
+        },
+        "components": {
+            "schemas": {
+                "ModelA": {
+                    "properties": {"a": {"type": "string", "title": "A"}},
+                    "type": "object",
+                    "required": ["a"],
+                    "title": "ModelA",
+                },
+                "ModelB": {
+                    "properties": {"b": {"type": "string", "title": "B"}},
+                    "type": "object",
+                    "required": ["b"],
+                    "title": "ModelB",
+                },
+            }
+        },
+    }