Repository: NRWLDev/fastapi-problem

Issue:
Pydantic friendly?
To integrate Fastapi with OpenAPI,   you can use [Additional Response with model](https://fastapi.tiangolo.com/advanced/additional-responses/#additional-response-with-model), and example as following: 

```
@router.post(
    "/collection",
    response_model=dict[str, Any],
    responses={400: {"model": BadRequestProblem}},
)
```

Fastapi will report error because BadRequestProblem is not a `BaseModel` from Pydantic. 

Discussion:
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/NRWLDev/fastapi-problem/pull/32?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NRWLDev) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`fd0bda6`)](https://app.codecov.io/gh/NRWLDev/fastapi-problem/commit/fd0bda627bbbc7c229bda001205b874ade83e3ad?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NRWLDev) to head [(`a0ed5a6`)](https://app.codecov.io/gh/NRWLDev/fastapi-problem/commit/a0ed5a60f9a3c3930d19145aac798cdf582bf8f7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NRWLDev).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main       #32   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         4           
  Lines           50        55    +5     
  Branches         5         5           
=========================================
+ Hits            50        55    +5     
```

| [Flag](https://app.codecov.io/gh/NRWLDev/fastapi-problem/pull/32/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NRWLDev) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/NRWLDev/fastapi-problem/pull/32/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NRWLDev) | `100.00% <100.00%> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NRWLDev#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/NRWLDev/fastapi-problem/pull/32?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NRWLDev).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NRWLDev).


Patch:
diff --git a/docs/error.md b/docs/error.md
index aaaead7..f69ee32 100644
--- a/docs/error.md
+++ b/docs/error.md
@@ -201,19 +201,3 @@ add_exception_handler(
     ...
 }
 ```
-
-### Swagger defaults
-
-When the exception handlers are registered, the default `422` response type is
-updated to match the Problem format instead of the FastAPI default response.
-
-A generic `4XX` and `5XX` response is added to each path as well, these can be
-opted out of by passing `generic_swagger_defaults=False` when registering the
-exception handlers.
-
-```python
-add_exception_handler(
-    app,
-    generic_swagger_defaults=False,
-)
-```
diff --git a/docs/usage.md b/docs/usage.md
index 8cfa006..d1e1d13 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -78,6 +78,41 @@ add_exception_handler(
 )
 ```
 
+## Swagger
+
+When the exception handlers are registered, the default `422` response type is
+updated to match the Problem format instead of the FastAPI default response.
+
+A generic `4XX` and `5XX` response is added to each path as well, these can be
+opted out of by passing `generic_swagger_defaults=False` when registering the
+exception handlers.
+
+```python
+add_exception_handler(
+    app,
+    generic_swagger_defaults=False,
+)
+```
+
+To specify specific error responses per endpoint, when registering the route
+the swagger responses for each possible error can be generated using the
+`generate_swagger_response` helper method.
+
+```
+from fastapi_problem.error import StatusProblem
+from fastapi_problem.handler import generate_swagger_response
+
+class NotFoundError(StatusProblem):
+    status = 404
+    title = "Endpoint not found."
+
+
+@app.post(
+    "/path",
+    responses={400: generate_swagger_response(NotFoundError)}},
+)
+...
+```
 ## Sentry
 
 `fastapi_problem` is designed to play nicely with [Sentry](https://sentry.io),
diff --git a/src/fastapi_problem/handler.py b/src/fastapi_problem/handler.py
index cbda655..e357ab1 100644
--- a/src/fastapi_problem/handler.py
+++ b/src/fastapi_problem/handler.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import json
 import typing as t
+from http.client import responses
 
 import rfc9457
 from fastapi.exceptions import RequestValidationError
@@ -27,6 +28,34 @@ if t.TYPE_CHECKING:
     from fastapi_problem.cors import CorsConfiguration
 
 
+def _swagger_problem_response(description: str, title: str, status: str, type_: str) -> dict:
+    return {
+        "description": description,
+        "content": {
+            "application/json": {
+                "schema": {
+                    "$ref": "#/components/schemas/Problem",
+                },
+                "example": {
+                    "title": title,
+                    "details": "Additional error context.",
+                    "type": type_,
+                    "status": status,
+                },
+            },
+        },
+    }
+
+
+def generate_swagger_response(exc: Problem) -> dict:
+    return _swagger_problem_response(
+        responses[exc.status],
+        title=exc.title,
+        type_=exc.type_ or rfc9457.error_class_to_type(exc("detail")),
+        status=exc.status,
+    )
+
+
 def customise_openapi(func: t.Callable[..., dict], *, generic_defaults: bool = True) -> t.Callable[..., dict]:
     """Customize OpenAPI schema."""
 
@@ -115,38 +144,18 @@ def customise_openapi(func: t.Callable[..., dict], *, generic_defaults: bool = T
         if generic_defaults:
             for methods in res["paths"].values():
                 for details in methods.values():
-                    details["responses"]["4XX"] = {
-                        "description": "Client Error",
-                        "content": {
-                            "application/json": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Problem",
-                                },
-                                "example": {
-                                    "title": "User facing error message.",
-                                    "details": "Additional error context.",
-                                    "type": "client-error-type",
-                                    "status": 400,
-                                },
-                            },
-                        },
-                    }
-                    details["responses"]["5XX"] = {
-                        "description": "Server Error",
-                        "content": {
-                            "application/json": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Problem",
-                                },
-                                "example": {
-                                    "title": "User facing error message.",
-                                    "details": "Additional error context.",
-                                    "type": "server-error-type",
-                                    "status": 500,
-                                },
-                            },
-                        },
-                    }
+                    details["responses"]["4XX"] = _swagger_problem_response(
+                        description="Client Error",
+                        title="User facing error message.",
+                        type_="client-error-type",
+                        status=400,
+                    )
+                    details["responses"]["5XX"] = _swagger_problem_response(
+                        description="Server Error",
+                        title="User facing error message.",
+                        type_="server-error-type",
+                        status=500,
+                    )
 
         return res
 


Test patch:
diff --git a/tests/test_handler.py b/tests/test_handler.py
index bb3dbd8..a003e75 100644
--- a/tests/test_handler.py
+++ b/tests/test_handler.py
@@ -441,6 +441,68 @@ async def test_exception_handler_in_app_post_register():
     }
 
 
+def test_swagger_problem_response():
+    assert handler._swagger_problem_response(
+        description="Client Error",
+        title="User facing error message.",
+        type_="client-error-type",
+        status=400,
+    ) == {
+        "content": {
+            "application/json": {
+                "schema": {
+                    "$ref": "#/components/schemas/Problem",
+                },
+                "example": {
+                    "title": "User facing error message.",
+                    "details": "Additional error context.",
+                    "type": "client-error-type",
+                    "status": 400,
+                },
+            },
+        },
+        "description": "Client Error",
+    }
+
+
+def test_generate_swagger_response_status_problem():
+    assert handler.generate_swagger_response(error.BadRequestProblem) == {
+        "content": {
+            "application/json": {
+                "schema": {
+                    "$ref": "#/components/schemas/Problem",
+                },
+                "example": {
+                    "title": "Base http exception.",
+                    "details": "Additional error context.",
+                    "type": "bad-request-problem",
+                    "status": 400,
+                },
+            },
+        },
+        "description": "Bad Request",
+    }
+
+
+def test_generate_swagger_response_custom_problem():
+    assert handler.generate_swagger_response(CustomUnhandledException) == {
+        "content": {
+            "application/json": {
+                "schema": {
+                    "$ref": "#/components/schemas/Problem",
+                },
+                "example": {
+                    "title": "Unhandled exception occurred.",
+                    "details": "Additional error context.",
+                    "type": "custom-unhandled-exception",
+                    "status": 500,
+                },
+            },
+        },
+        "description": "Internal Server Error",
+    }
+
+
 async def test_customise_openapi():
     app = FastAPI()