From 2d3c30348df011cc4e98dfa0a7c78cdb39bbb93b Mon Sep 17 00:00:00 2001 From: Adam Stewart Date: Thu, 21 Aug 2025 16:26:09 +0100 Subject: [PATCH 1/2] fix: Fix OpenAPI Swagger docs in certain scenarios Namely when paths have no validation errors so there are no components, or in the same situation when a security scheme is used that creates a components without the necessary schemas key --- src/fastapi_problem/handler.py | 8 +- tests/test_handler.py | 187 ++++++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 3 deletions(-) diff --git a/src/fastapi_problem/handler.py b/src/fastapi_problem/handler.py index 2dc81fd..64db81e 100644 --- a/src/fastapi_problem/handler.py +++ b/src/fastapi_problem/handler.py @@ -84,9 +84,15 @@ def wrapper() -> dict: """Wrapper.""" res = func() - if "components" not in res: + if not res["paths"]: + # If there are no paths, we don't need to add any responses return res + if "components" not in res: + res["components"] = {"schemas": {}} + elif "schemas" not in res["components"]: + res["components"]["schemas"] = {} + validation_error = problem_component( "RequestValidationError", required=["errors"], diff --git a/tests/test_handler.py b/tests/test_handler.py index 840fcba..c96c022 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -4,8 +4,9 @@ import httpx import pytest -from fastapi import FastAPI +from fastapi import Depends, FastAPI from fastapi.exceptions import RequestValidationError +from fastapi.security import HTTPBearer from starlette.exceptions import HTTPException from fastapi_problem import error, handler @@ -702,7 +703,7 @@ async def status(_a: str) -> dict: } -async def test_customise_openapi_handles_no_components(): +async def test_customise_openapi_handles_no_components_no_paths(): app = FastAPI() app.openapi = handler.customise_openapi(app.openapi) @@ -712,6 +713,188 @@ async def test_customise_openapi_handles_no_components(): assert "components" not in res +async def test_customise_openapi_handles_no_components_no_422(): + app = FastAPI() + + @app.get("/status") + async def status() -> dict: + return {} + + app.openapi = handler.customise_openapi(app.openapi) + + res = app.openapi() + + assert res["components"]["schemas"]["HTTPValidationError"] == { + "properties": { + "title": { + "type": "string", + "title": "Problem title", + }, + "type": { + "type": "string", + "title": "Problem type", + }, + "status": { + "type": "integer", + "title": "Status code", + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + }, + }, + "type": "object", + "required": [ + "type", + "title", + "status", + "errors", + ], + "title": "RequestValidationError", + } + assert "Problem" in res["components"]["schemas"] + + assert res["paths"]["/status"]["get"]["responses"] == { + "200": { + "content": { + "application/json": { + "schema": { + "title": "Response Status Status Get", + "type": "object", + }, + }, + }, + "description": "Successful Response", + }, + "4XX": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem", + }, + "example": { + "title": "User facing error message.", + "detail": "Additional error context.", + "type": "client-error-type", + "status": 400, + }, + }, + }, + "description": "Client Error", + }, + "5XX": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem", + }, + "example": { + "title": "User facing error message.", + "detail": "Additional error context.", + "type": "server-error-type", + "status": 500, + }, + }, + }, + "description": "Server Error", + }, + } + + +async def test_customise_openapi_handles_security_components_no_422(): + bearer_scheme = HTTPBearer(bearerFormat="JWT") + app = FastAPI() + + @app.get("/status") + async def status(bearer: str = Depends(bearer_scheme)) -> dict: + return {} + + app.openapi = handler.customise_openapi(app.openapi) + + res = app.openapi() + + assert res["components"]["schemas"]["HTTPValidationError"] == { + "properties": { + "title": { + "type": "string", + "title": "Problem title", + }, + "type": { + "type": "string", + "title": "Problem type", + }, + "status": { + "type": "integer", + "title": "Status code", + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError", + }, + }, + }, + "type": "object", + "required": [ + "type", + "title", + "status", + "errors", + ], + "title": "RequestValidationError", + } + assert "Problem" in res["components"]["schemas"] + assert "securitySchemes" in res["components"] + + assert res["paths"]["/status"]["get"]["responses"] == { + "200": { + "content": { + "application/json": { + "schema": { + "title": "Response Status Status Get", + "type": "object", + }, + }, + }, + "description": "Successful Response", + }, + "4XX": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem", + }, + "example": { + "title": "User facing error message.", + "detail": "Additional error context.", + "type": "client-error-type", + "status": 400, + }, + }, + }, + "description": "Client Error", + }, + "5XX": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem", + }, + "example": { + "title": "User facing error message.", + "detail": "Additional error context.", + "type": "server-error-type", + "status": 500, + }, + }, + }, + "description": "Server Error", + }, + } + + async def test_customise_openapi_generic_opt_out(): app = FastAPI() From a5dac3768a80676bc1754626be078b6d21e9d9bc Mon Sep 17 00:00:00 2001 From: Adam Stewart Date: Sat, 23 Aug 2025 19:24:33 +0100 Subject: [PATCH 2/2] Ignore ARG001 in tests --- tests/test_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_handler.py b/tests/test_handler.py index c96c022..26b3f75 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -808,7 +808,7 @@ async def test_customise_openapi_handles_security_components_no_422(): app = FastAPI() @app.get("/status") - async def status(bearer: str = Depends(bearer_scheme)) -> dict: + async def status(bearer: str = Depends(bearer_scheme)) -> dict: # noqa: ARG001 return {} app.openapi = handler.customise_openapi(app.openapi)