fastapi-errors-plus 0.7.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/PKG-INFO +152 -30
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/README.md +148 -29
- fastapi_errors_plus-0.9.0/fastapi_errors_plus/__init__.py +19 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/fastapi_errors_plus/base.py +49 -57
- fastapi_errors_plus-0.9.0/fastapi_errors_plus/error_doc.py +65 -0
- fastapi_errors_plus-0.9.0/fastapi_errors_plus/error_profile.py +31 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/fastapi_errors_plus/errors.py +293 -232
- fastapi_errors_plus-0.9.0/fastapi_errors_plus/example_utils.py +52 -0
- fastapi_errors_plus-0.9.0/fastapi_errors_plus/merge_utils.py +61 -0
- fastapi_errors_plus-0.9.0/fastapi_errors_plus/protocol.py +40 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/fastapi_errors_plus.egg-info/PKG-INFO +152 -30
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/fastapi_errors_plus.egg-info/SOURCES.txt +8 -1
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/fastapi_errors_plus.egg-info/requires.txt +3 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/pyproject.toml +22 -1
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/tests/test_app.py +26 -18
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/tests/test_base.py +72 -61
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/tests/test_errors.py +248 -181
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/tests/test_integration.py +93 -92
- fastapi_errors_plus-0.9.0/tests/test_release_08.py +131 -0
- fastapi_errors_plus-0.9.0/tests/test_release_08_edge_cases.py +243 -0
- fastapi_errors_plus-0.9.0/tests/test_release_09.py +122 -0
- fastapi_errors_plus-0.7.0/fastapi_errors_plus/__init__.py +0 -14
- fastapi_errors_plus-0.7.0/fastapi_errors_plus/protocol.py +0 -60
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/LICENSE +0 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/fastapi_errors_plus/py.typed +0 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/fastapi_errors_plus.egg-info/dependency_links.txt +0 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/fastapi_errors_plus.egg-info/top_level.txt +0 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.9.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-errors-plus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Universal library for documenting errors in FastAPI endpoints
|
|
5
5
|
Author-email: Igor Selivanov <seligoroff@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -22,6 +22,9 @@ Provides-Extra: dev
|
|
|
22
22
|
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
23
23
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
24
24
|
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
25
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
25
28
|
Dynamic: license-file
|
|
26
29
|
|
|
27
30
|
# fastapi-errors-plus
|
|
@@ -30,7 +33,7 @@ Dynamic: license-file
|
|
|
30
33
|
[](https://opensource.org/licenses/MIT)
|
|
31
34
|
[](https://www.python.org/downloads/)
|
|
32
35
|
[](https://fastapi.tiangolo.com/)
|
|
33
|
-
[](https://github.com/seligoroff/fastapi-errors-plus)
|
|
34
37
|
[](https://github.com/seligoroff/fastapi-errors-plus)
|
|
35
38
|
|
|
36
39
|
Universal library for documenting errors in FastAPI endpoints.
|
|
@@ -105,13 +108,13 @@ Use boolean flags for common HTTP status codes:
|
|
|
105
108
|
- `validation_error_422=True` → 422 Unprocessable Entity (defaults to `True`)
|
|
106
109
|
- `internal_server_error_500=True` → 500 Internal Server Error
|
|
107
110
|
|
|
108
|
-
**Legacy (
|
|
111
|
+
**Legacy (deprecated in 0.9, removal in 1.0):**
|
|
109
112
|
- `unauthorized=True` → 401 Unauthorized
|
|
110
113
|
- `forbidden=True` → 403 Forbidden
|
|
111
|
-
- `validation_error=True` → 422 Unprocessable Entity
|
|
114
|
+
- `validation_error=True` → 422 Unprocessable Entity
|
|
112
115
|
- `internal_server_error=True` → 500 Internal Server Error
|
|
113
116
|
|
|
114
|
-
**Note:** `validation_error` and `validation_error_422
|
|
117
|
+
**Note on 422:** If you omit both `validation_error` and `validation_error_422`, the library still adds **422** today but emits a **`DeprecationWarning`** — the default will become **`False` in 1.0**. For ADR-style APIs, set `validation_error_422=False` explicitly (or use **`ErrorProfile`** below).
|
|
115
118
|
|
|
116
119
|
```python
|
|
117
120
|
@router.get(
|
|
@@ -164,7 +167,7 @@ class MyErrorDTO:
|
|
|
164
167
|
status_code = 404
|
|
165
168
|
message = "Not found"
|
|
166
169
|
|
|
167
|
-
def
|
|
170
|
+
def to_examples(self):
|
|
168
171
|
return {
|
|
169
172
|
"Not found": {
|
|
170
173
|
"value": {"detail": "Not found"},
|
|
@@ -209,7 +212,7 @@ def delete_item(id: int):
|
|
|
209
212
|
|
|
210
213
|
#### OpenAPI extras (`schema`) next to examples
|
|
211
214
|
|
|
212
|
-
ADR-style payloads (`code`, `detail`, optional `context`) need a **`schema`** in the spec besides **`examples`**. On **`BaseErrorDTO`** / **`StandardErrorDTO`** use **`openapi_json_extras`** (a dict merged under `content["application/json"]` — omit `example` / `examples` there; **`
|
|
215
|
+
ADR-style payloads (`code`, `detail`, optional `context`) need a **`schema`** in the spec besides **`examples`**. On **`BaseErrorDTO`** / **`StandardErrorDTO`** / **`ErrorDoc`** use **`openapi_json_extras`** (a dict merged under `content["application/json"]` — omit `example` / `examples` there; **`to_examples()`** still defines examples):
|
|
213
216
|
|
|
214
217
|
```python
|
|
215
218
|
from fastapi import APIRouter, status
|
|
@@ -284,6 +287,75 @@ def delete_item(id: int):
|
|
|
284
287
|
- Reusable across all endpoints
|
|
285
288
|
- Supports inheritance for custom logic
|
|
286
289
|
|
|
290
|
+
`examples` values may be **strings** (shorthand for `{"detail": text}`) or full OpenAPI Example Objects with **`summary`** and **`value`**.
|
|
291
|
+
|
|
292
|
+
#### ErrorDoc
|
|
293
|
+
|
|
294
|
+
For arbitrary response bodies (ADR `code` / `detail` / `context`, not only `detail` strings), use **`ErrorDoc`**:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from fastapi_errors_plus import ErrorDoc, Errors
|
|
298
|
+
|
|
299
|
+
permission_denied = ErrorDoc(
|
|
300
|
+
status_code=403,
|
|
301
|
+
message="Insufficient permissions",
|
|
302
|
+
examples={
|
|
303
|
+
"MissingRole": {
|
|
304
|
+
"summary": "User lacks required role",
|
|
305
|
+
"value": {
|
|
306
|
+
"code": "FORBIDDEN",
|
|
307
|
+
"detail": "Role admin required",
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
openapi_json_extras={"schema": ADR_ERROR_BODY_SCHEMA},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
@router.delete("/{id}", responses=Errors(permission_denied))
|
|
315
|
+
def delete_item(id: int):
|
|
316
|
+
...
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
A **plain dict** as an example value is treated as the full response **body** unless it looks like an OpenAPI Example Object (keys only among `value`, `summary`, `description`, `externalValue`).
|
|
320
|
+
|
|
321
|
+
Optional **`model=`** (Pydantic model on the outer response object for FastAPI `$ref` registration) and **`schema=`** (JSON Schema under `application/json`) avoid a separate status `dict` for typed error bodies:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from fastapi_errors_plus import ErrorDoc, Errors
|
|
325
|
+
|
|
326
|
+
conflict = ErrorDoc(
|
|
327
|
+
status_code=409,
|
|
328
|
+
message="BusinessRule",
|
|
329
|
+
model=ApplicationJsonError, # optional Pydantic model
|
|
330
|
+
schema=ADR_ERROR_BODY_SCHEMA, # or raw JSON Schema
|
|
331
|
+
body={"code": "RULE_VIOLATION", "detail": "Item exists"},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
@router.post("/items", responses=Errors(conflict, validation_error_422=False))
|
|
335
|
+
def create_item():
|
|
336
|
+
...
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### ErrorProfile
|
|
340
|
+
|
|
341
|
+
Project-wide defaults (frozen — not mutated by endpoint calls):
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
from fastapi_errors_plus import ErrorDoc, ErrorProfile, Errors
|
|
345
|
+
|
|
346
|
+
ADR = ErrorProfile(
|
|
347
|
+
validation_error_422=False,
|
|
348
|
+
unauthorized_401=True,
|
|
349
|
+
internal_server_error_500=True,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
@router.post("/items", responses=Errors(business_conflict, profile=ADR))
|
|
353
|
+
def create_item():
|
|
354
|
+
...
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Explicit `Errors` keyword flags override profile values. Positional dict/DTO errors are merged after profile-driven standard statuses.
|
|
358
|
+
|
|
287
359
|
### 5. Mixed Usage
|
|
288
360
|
|
|
289
361
|
Combine flags, dict, and ErrorDTO:
|
|
@@ -363,7 +435,7 @@ Order matters only for overlaps: whichever **`dict`** is applied **later** in th
|
|
|
363
435
|
|
|
364
436
|
## ErrorDTO Protocol
|
|
365
437
|
|
|
366
|
-
The
|
|
438
|
+
The canonical **`ErrorDTO`** protocol defines the interface for error objects compatible with the library:
|
|
367
439
|
|
|
368
440
|
```python
|
|
369
441
|
from typing import Protocol, Dict, Any
|
|
@@ -372,27 +444,30 @@ class ErrorDTO(Protocol):
|
|
|
372
444
|
status_code: int
|
|
373
445
|
message: str
|
|
374
446
|
|
|
375
|
-
def
|
|
376
|
-
"""
|
|
377
|
-
|
|
378
|
-
Returns:
|
|
379
|
-
Dict in format: {"key": {"value": {"detail": "message"}}}
|
|
380
|
-
"""
|
|
447
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
448
|
+
"""OpenAPI ``examples`` map: {"Key": {"value": {...}, "summary": "..."}}."""
|
|
381
449
|
...
|
|
382
450
|
```
|
|
383
451
|
|
|
384
452
|
Any class implementing this protocol (through structural typing) can be used with `Errors()`.
|
|
385
453
|
|
|
454
|
+
**Legacy migration:** classes that only define **`to_example()`** still work at **runtime** (`DeprecationWarning` once per class per `Errors()` call). For static typing use **`LegacyErrorDTO`** or the union alias **`ErrorDTOLike`** (`ErrorDTO | LegacyErrorDTO`).
|
|
455
|
+
|
|
386
456
|
**Best Practice:** For maximum clarity, consider making your domain exceptions implement the ErrorDTO protocol directly. See [Best Practice: Connecting Exceptions and ErrorDTO](#best-practice-connecting-exceptions-and-errordto) for details.
|
|
387
457
|
|
|
388
458
|
### When to Use Protocol vs BaseErrorDTO
|
|
389
459
|
|
|
460
|
+
Optional on custom DTOs (OpenAPI media-type extras without duplicating a status `dict`):
|
|
461
|
+
|
|
462
|
+
- attribute **`openapi_json_extras`**: fragment for **`content["application/json"]`** (often `{"schema": ...}` — not for `example` / `examples`);
|
|
463
|
+
- or method **`to_openapi_json_media_type_extras() -> Optional[dict]`** — when non-empty, overrides **`openapi_json_extras`**.
|
|
464
|
+
|
|
390
465
|
**Use Protocol (structural typing)** when:
|
|
391
466
|
- Your project already has error DTOs that implement the protocol
|
|
392
467
|
- You need maximum flexibility and custom implementations
|
|
393
468
|
- You want to keep your existing error infrastructure
|
|
394
469
|
|
|
395
|
-
**Use BaseErrorDTO/StandardErrorDTO** when:
|
|
470
|
+
**Use BaseErrorDTO/StandardErrorDTO/ErrorDoc** when:
|
|
396
471
|
- Starting a new project or adding error documentation
|
|
397
472
|
- You want a ready-to-use implementation without boilerplate
|
|
398
473
|
- You need multiple examples for standard HTTP errors (401, 403, etc.)
|
|
@@ -403,7 +478,7 @@ Both approaches work together — you can mix them in the same `Errors()` call!
|
|
|
403
478
|
|
|
404
479
|
**Note:** Pydantic is **not required** to use this library. This section is for projects that already use Pydantic and want to integrate it with the ErrorDTO protocol.
|
|
405
480
|
|
|
406
|
-
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `
|
|
481
|
+
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_examples()`) will work, including Pydantic models. Legacy `to_example()` is still accepted at runtime.
|
|
407
482
|
|
|
408
483
|
### Simple Pydantic Model as ErrorDTO
|
|
409
484
|
|
|
@@ -417,8 +492,8 @@ class PydanticErrorDTO(BaseModel):
|
|
|
417
492
|
status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
|
|
418
493
|
message: str = Field(..., min_length=1, description="Error message")
|
|
419
494
|
|
|
420
|
-
def
|
|
421
|
-
"""Generate
|
|
495
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
496
|
+
"""Generate examples for OpenAPI."""
|
|
422
497
|
return {
|
|
423
498
|
self.message: {
|
|
424
499
|
"value": {"detail": self.message},
|
|
@@ -461,8 +536,8 @@ class DetailedErrorDTO(BaseModel):
|
|
|
461
536
|
error_code: Optional[str] = Field(None, description="Internal error code")
|
|
462
537
|
timestamp: Optional[str] = Field(None, description="Error timestamp")
|
|
463
538
|
|
|
464
|
-
def
|
|
465
|
-
"""Generate
|
|
539
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
540
|
+
"""Generate examples for OpenAPI."""
|
|
466
541
|
example = {"detail": self.message}
|
|
467
542
|
if self.error_code:
|
|
468
543
|
example["error_code"] = self.error_code
|
|
@@ -519,7 +594,7 @@ class DomainException(Exception):
|
|
|
519
594
|
status_code: int
|
|
520
595
|
message: str
|
|
521
596
|
|
|
522
|
-
def
|
|
597
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
523
598
|
return {self.message: {"value": {"detail": self.message}}}
|
|
524
599
|
|
|
525
600
|
@classmethod
|
|
@@ -562,7 +637,7 @@ See [examples/domain_exceptions.py](examples/domain_exceptions.py) for complete
|
|
|
562
637
|
|
|
563
638
|
## Compatibility with Existing Projects
|
|
564
639
|
|
|
565
|
-
If your project already has error DTOs (like `ApiErrorDTO`), they can work with `fastapi-errors-plus` if they implement the `ErrorDTO` protocol:
|
|
640
|
+
If your project already has error DTOs (like `ApiErrorDTO`), they can work with `fastapi-errors-plus` if they implement the `ErrorDTO` protocol (or legacy `to_example()` during migration):
|
|
566
641
|
|
|
567
642
|
```python
|
|
568
643
|
# Your existing ApiErrorDTO
|
|
@@ -571,7 +646,7 @@ class ApiErrorDTO:
|
|
|
571
646
|
status_code: int
|
|
572
647
|
message: str
|
|
573
648
|
|
|
574
|
-
def
|
|
649
|
+
def to_examples(self) -> dict:
|
|
575
650
|
return {
|
|
576
651
|
self.message: {
|
|
577
652
|
"value": {"detail": self.message},
|
|
@@ -608,6 +683,7 @@ Errors(
|
|
|
608
683
|
forbidden_403: bool = False,
|
|
609
684
|
validation_error_422: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
|
|
610
685
|
internal_server_error_500: bool = False,
|
|
686
|
+
profile: Optional[ErrorProfile] = None,
|
|
611
687
|
)
|
|
612
688
|
```
|
|
613
689
|
|
|
@@ -626,10 +702,12 @@ Errors(
|
|
|
626
702
|
- `None` (default): Add 422 (True by default, FastAPI validates all parameters)
|
|
627
703
|
- `False`: Explicitly disable 422
|
|
628
704
|
- `True`: Explicitly enable 422
|
|
629
|
-
- `internal_server_error`: Add 500 Internal Server Error (legacy,
|
|
705
|
+
- `internal_server_error`: Add 500 Internal Server Error (legacy, **deprecated** in 0.9). Defaults to `False`.
|
|
706
|
+
- `profile`: Optional **`ErrorProfile`** — project defaults; explicit kwargs override profile.
|
|
707
|
+
|
|
708
|
+
**Deprecation (0.9+):** legacy kwargs above emit `DeprecationWarning` (removal in **1.0**). Omitting both `validation_error` and `validation_error_422` still adds 422 but warns; default becomes **`False` in 1.0**.
|
|
630
709
|
|
|
631
|
-
**
|
|
632
|
-
FastAPI automatically validates all parameters (Path, Query, Body), so 422 is relevant in 95%+ of endpoints. For endpoints without parameters, explicitly set `validation_error=False` or `validation_error_422=False`.
|
|
710
|
+
**422 behaviour:** pass `validation_error_422=False` for endpoints without request-body validation or ADR-style error bodies. Use **`ErrorProfile`** to set this once per project.
|
|
633
711
|
|
|
634
712
|
**Returns:**
|
|
635
713
|
- A dict-like `Mapping[int, …]` suitable for FastAPI’s `responses` / OpenAPI
|
|
@@ -640,9 +718,13 @@ FastAPI automatically validates all parameters (Path, Query, Body), so 422 is re
|
|
|
640
718
|
```python
|
|
641
719
|
# Instances are Mapping keyed by HTTP status codes
|
|
642
720
|
error_responses = Errors(unauthorized_401=True, forbidden_403=True)
|
|
643
|
-
documented = error_responses[401] #
|
|
721
|
+
documented = error_responses[401] # deep copy — safe to read, won't mutate internal state
|
|
644
722
|
```
|
|
645
723
|
|
|
724
|
+
**Isolation (0.8+):** incoming response **dicts** are **deep-copied** on ingest; **`errors[status]`** returns a **deep copy** so callers cannot corrupt shared registry templates or internal merge state.
|
|
725
|
+
|
|
726
|
+
**Descriptions:** response blocks with only **`model`** (no `description`) get a default from **`HTTPStatus.phrase`** so OpenAPI generation does not fail.
|
|
727
|
+
|
|
646
728
|
### `ErrorDTO`
|
|
647
729
|
|
|
648
730
|
Protocol for error objects compatible with the library.
|
|
@@ -652,9 +734,47 @@ Protocol for error objects compatible with the library.
|
|
|
652
734
|
- `message: str` — Error message description
|
|
653
735
|
|
|
654
736
|
**Required methods:**
|
|
655
|
-
- `
|
|
737
|
+
- `to_examples() -> Dict[str, Any]` — OpenAPI `examples` map for `application/json`
|
|
738
|
+
|
|
739
|
+
During `Errors(...)` initialization, non-`dict` objects in `*errors` missing `status_code`, `message`, or a callable **`to_examples()`** / **`to_example()`** raise **`TypeError`** naming what was missing.
|
|
740
|
+
|
|
741
|
+
### `LegacyErrorDTO` / `ErrorDTOLike`
|
|
656
742
|
|
|
657
|
-
|
|
743
|
+
- **`LegacyErrorDTO`** — typing helper for classes that only implement deprecated **`to_example()`**.
|
|
744
|
+
- **`ErrorDTOLike`** — `Union[ErrorDTO, LegacyErrorDTO]` for transitional annotations.
|
|
745
|
+
|
|
746
|
+
### `ErrorDoc`
|
|
747
|
+
|
|
748
|
+
Declarative DTO for arbitrary example bodies and per-example **`summary`**.
|
|
749
|
+
|
|
750
|
+
**Constructor:**
|
|
751
|
+
```python
|
|
752
|
+
ErrorDoc(
|
|
753
|
+
status_code: int,
|
|
754
|
+
message: str,
|
|
755
|
+
examples: Optional[Dict[str, str | dict]] = None,
|
|
756
|
+
body: Optional[Dict[str, Any]] = None,
|
|
757
|
+
example_key: Optional[str] = None,
|
|
758
|
+
model: Any = None,
|
|
759
|
+
schema: Optional[Dict[str, Any]] = None,
|
|
760
|
+
openapi_json_extras: Optional[Dict[str, Any]] = None,
|
|
761
|
+
)
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
When **`examples`** is omitted, a single example is built from **`body`** or `{"detail": message}`.
|
|
765
|
+
|
|
766
|
+
### `ErrorProfile`
|
|
767
|
+
|
|
768
|
+
Frozen project-wide defaults for standard HTTP flags.
|
|
769
|
+
|
|
770
|
+
```python
|
|
771
|
+
ErrorProfile(
|
|
772
|
+
unauthorized_401: bool = False,
|
|
773
|
+
forbidden_403: bool = False,
|
|
774
|
+
validation_error_422: Optional[bool] = None,
|
|
775
|
+
internal_server_error_500: bool = False,
|
|
776
|
+
)
|
|
777
|
+
```
|
|
658
778
|
|
|
659
779
|
### `BaseErrorDTO`
|
|
660
780
|
|
|
@@ -665,6 +785,8 @@ Base implementation of ErrorDTO Protocol for convenience.
|
|
|
665
785
|
BaseErrorDTO(
|
|
666
786
|
status_code: int,
|
|
667
787
|
message: str,
|
|
788
|
+
model: Any = None,
|
|
789
|
+
schema: Optional[Dict[str, Any]] = None,
|
|
668
790
|
openapi_json_extras: Optional[Dict[str, Any]] = None,
|
|
669
791
|
)
|
|
670
792
|
```
|
|
@@ -793,7 +915,7 @@ class DomainException(Exception):
|
|
|
793
915
|
def __init__(self) -> None:
|
|
794
916
|
super().__init__(self.message)
|
|
795
917
|
|
|
796
|
-
def
|
|
918
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
797
919
|
return {
|
|
798
920
|
self.message: {
|
|
799
921
|
"value": {"detail": self.message},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://www.python.org/downloads/)
|
|
6
6
|
[](https://fastapi.tiangolo.com/)
|
|
7
|
-
[](https://github.com/seligoroff/fastapi-errors-plus)
|
|
8
8
|
[](https://github.com/seligoroff/fastapi-errors-plus)
|
|
9
9
|
|
|
10
10
|
Universal library for documenting errors in FastAPI endpoints.
|
|
@@ -79,13 +79,13 @@ Use boolean flags for common HTTP status codes:
|
|
|
79
79
|
- `validation_error_422=True` → 422 Unprocessable Entity (defaults to `True`)
|
|
80
80
|
- `internal_server_error_500=True` → 500 Internal Server Error
|
|
81
81
|
|
|
82
|
-
**Legacy (
|
|
82
|
+
**Legacy (deprecated in 0.9, removal in 1.0):**
|
|
83
83
|
- `unauthorized=True` → 401 Unauthorized
|
|
84
84
|
- `forbidden=True` → 403 Forbidden
|
|
85
|
-
- `validation_error=True` → 422 Unprocessable Entity
|
|
85
|
+
- `validation_error=True` → 422 Unprocessable Entity
|
|
86
86
|
- `internal_server_error=True` → 500 Internal Server Error
|
|
87
87
|
|
|
88
|
-
**Note:** `validation_error` and `validation_error_422
|
|
88
|
+
**Note on 422:** If you omit both `validation_error` and `validation_error_422`, the library still adds **422** today but emits a **`DeprecationWarning`** — the default will become **`False` in 1.0**. For ADR-style APIs, set `validation_error_422=False` explicitly (or use **`ErrorProfile`** below).
|
|
89
89
|
|
|
90
90
|
```python
|
|
91
91
|
@router.get(
|
|
@@ -138,7 +138,7 @@ class MyErrorDTO:
|
|
|
138
138
|
status_code = 404
|
|
139
139
|
message = "Not found"
|
|
140
140
|
|
|
141
|
-
def
|
|
141
|
+
def to_examples(self):
|
|
142
142
|
return {
|
|
143
143
|
"Not found": {
|
|
144
144
|
"value": {"detail": "Not found"},
|
|
@@ -183,7 +183,7 @@ def delete_item(id: int):
|
|
|
183
183
|
|
|
184
184
|
#### OpenAPI extras (`schema`) next to examples
|
|
185
185
|
|
|
186
|
-
ADR-style payloads (`code`, `detail`, optional `context`) need a **`schema`** in the spec besides **`examples`**. On **`BaseErrorDTO`** / **`StandardErrorDTO`** use **`openapi_json_extras`** (a dict merged under `content["application/json"]` — omit `example` / `examples` there; **`
|
|
186
|
+
ADR-style payloads (`code`, `detail`, optional `context`) need a **`schema`** in the spec besides **`examples`**. On **`BaseErrorDTO`** / **`StandardErrorDTO`** / **`ErrorDoc`** use **`openapi_json_extras`** (a dict merged under `content["application/json"]` — omit `example` / `examples` there; **`to_examples()`** still defines examples):
|
|
187
187
|
|
|
188
188
|
```python
|
|
189
189
|
from fastapi import APIRouter, status
|
|
@@ -258,6 +258,75 @@ def delete_item(id: int):
|
|
|
258
258
|
- Reusable across all endpoints
|
|
259
259
|
- Supports inheritance for custom logic
|
|
260
260
|
|
|
261
|
+
`examples` values may be **strings** (shorthand for `{"detail": text}`) or full OpenAPI Example Objects with **`summary`** and **`value`**.
|
|
262
|
+
|
|
263
|
+
#### ErrorDoc
|
|
264
|
+
|
|
265
|
+
For arbitrary response bodies (ADR `code` / `detail` / `context`, not only `detail` strings), use **`ErrorDoc`**:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from fastapi_errors_plus import ErrorDoc, Errors
|
|
269
|
+
|
|
270
|
+
permission_denied = ErrorDoc(
|
|
271
|
+
status_code=403,
|
|
272
|
+
message="Insufficient permissions",
|
|
273
|
+
examples={
|
|
274
|
+
"MissingRole": {
|
|
275
|
+
"summary": "User lacks required role",
|
|
276
|
+
"value": {
|
|
277
|
+
"code": "FORBIDDEN",
|
|
278
|
+
"detail": "Role admin required",
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
openapi_json_extras={"schema": ADR_ERROR_BODY_SCHEMA},
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
@router.delete("/{id}", responses=Errors(permission_denied))
|
|
286
|
+
def delete_item(id: int):
|
|
287
|
+
...
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
A **plain dict** as an example value is treated as the full response **body** unless it looks like an OpenAPI Example Object (keys only among `value`, `summary`, `description`, `externalValue`).
|
|
291
|
+
|
|
292
|
+
Optional **`model=`** (Pydantic model on the outer response object for FastAPI `$ref` registration) and **`schema=`** (JSON Schema under `application/json`) avoid a separate status `dict` for typed error bodies:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from fastapi_errors_plus import ErrorDoc, Errors
|
|
296
|
+
|
|
297
|
+
conflict = ErrorDoc(
|
|
298
|
+
status_code=409,
|
|
299
|
+
message="BusinessRule",
|
|
300
|
+
model=ApplicationJsonError, # optional Pydantic model
|
|
301
|
+
schema=ADR_ERROR_BODY_SCHEMA, # or raw JSON Schema
|
|
302
|
+
body={"code": "RULE_VIOLATION", "detail": "Item exists"},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
@router.post("/items", responses=Errors(conflict, validation_error_422=False))
|
|
306
|
+
def create_item():
|
|
307
|
+
...
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
#### ErrorProfile
|
|
311
|
+
|
|
312
|
+
Project-wide defaults (frozen — not mutated by endpoint calls):
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from fastapi_errors_plus import ErrorDoc, ErrorProfile, Errors
|
|
316
|
+
|
|
317
|
+
ADR = ErrorProfile(
|
|
318
|
+
validation_error_422=False,
|
|
319
|
+
unauthorized_401=True,
|
|
320
|
+
internal_server_error_500=True,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
@router.post("/items", responses=Errors(business_conflict, profile=ADR))
|
|
324
|
+
def create_item():
|
|
325
|
+
...
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Explicit `Errors` keyword flags override profile values. Positional dict/DTO errors are merged after profile-driven standard statuses.
|
|
329
|
+
|
|
261
330
|
### 5. Mixed Usage
|
|
262
331
|
|
|
263
332
|
Combine flags, dict, and ErrorDTO:
|
|
@@ -337,7 +406,7 @@ Order matters only for overlaps: whichever **`dict`** is applied **later** in th
|
|
|
337
406
|
|
|
338
407
|
## ErrorDTO Protocol
|
|
339
408
|
|
|
340
|
-
The
|
|
409
|
+
The canonical **`ErrorDTO`** protocol defines the interface for error objects compatible with the library:
|
|
341
410
|
|
|
342
411
|
```python
|
|
343
412
|
from typing import Protocol, Dict, Any
|
|
@@ -346,27 +415,30 @@ class ErrorDTO(Protocol):
|
|
|
346
415
|
status_code: int
|
|
347
416
|
message: str
|
|
348
417
|
|
|
349
|
-
def
|
|
350
|
-
"""
|
|
351
|
-
|
|
352
|
-
Returns:
|
|
353
|
-
Dict in format: {"key": {"value": {"detail": "message"}}}
|
|
354
|
-
"""
|
|
418
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
419
|
+
"""OpenAPI ``examples`` map: {"Key": {"value": {...}, "summary": "..."}}."""
|
|
355
420
|
...
|
|
356
421
|
```
|
|
357
422
|
|
|
358
423
|
Any class implementing this protocol (through structural typing) can be used with `Errors()`.
|
|
359
424
|
|
|
425
|
+
**Legacy migration:** classes that only define **`to_example()`** still work at **runtime** (`DeprecationWarning` once per class per `Errors()` call). For static typing use **`LegacyErrorDTO`** or the union alias **`ErrorDTOLike`** (`ErrorDTO | LegacyErrorDTO`).
|
|
426
|
+
|
|
360
427
|
**Best Practice:** For maximum clarity, consider making your domain exceptions implement the ErrorDTO protocol directly. See [Best Practice: Connecting Exceptions and ErrorDTO](#best-practice-connecting-exceptions-and-errordto) for details.
|
|
361
428
|
|
|
362
429
|
### When to Use Protocol vs BaseErrorDTO
|
|
363
430
|
|
|
431
|
+
Optional on custom DTOs (OpenAPI media-type extras without duplicating a status `dict`):
|
|
432
|
+
|
|
433
|
+
- attribute **`openapi_json_extras`**: fragment for **`content["application/json"]`** (often `{"schema": ...}` — not for `example` / `examples`);
|
|
434
|
+
- or method **`to_openapi_json_media_type_extras() -> Optional[dict]`** — when non-empty, overrides **`openapi_json_extras`**.
|
|
435
|
+
|
|
364
436
|
**Use Protocol (structural typing)** when:
|
|
365
437
|
- Your project already has error DTOs that implement the protocol
|
|
366
438
|
- You need maximum flexibility and custom implementations
|
|
367
439
|
- You want to keep your existing error infrastructure
|
|
368
440
|
|
|
369
|
-
**Use BaseErrorDTO/StandardErrorDTO** when:
|
|
441
|
+
**Use BaseErrorDTO/StandardErrorDTO/ErrorDoc** when:
|
|
370
442
|
- Starting a new project or adding error documentation
|
|
371
443
|
- You want a ready-to-use implementation without boilerplate
|
|
372
444
|
- You need multiple examples for standard HTTP errors (401, 403, etc.)
|
|
@@ -377,7 +449,7 @@ Both approaches work together — you can mix them in the same `Errors()` call!
|
|
|
377
449
|
|
|
378
450
|
**Note:** Pydantic is **not required** to use this library. This section is for projects that already use Pydantic and want to integrate it with the ErrorDTO protocol.
|
|
379
451
|
|
|
380
|
-
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `
|
|
452
|
+
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_examples()`) will work, including Pydantic models. Legacy `to_example()` is still accepted at runtime.
|
|
381
453
|
|
|
382
454
|
### Simple Pydantic Model as ErrorDTO
|
|
383
455
|
|
|
@@ -391,8 +463,8 @@ class PydanticErrorDTO(BaseModel):
|
|
|
391
463
|
status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
|
|
392
464
|
message: str = Field(..., min_length=1, description="Error message")
|
|
393
465
|
|
|
394
|
-
def
|
|
395
|
-
"""Generate
|
|
466
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
467
|
+
"""Generate examples for OpenAPI."""
|
|
396
468
|
return {
|
|
397
469
|
self.message: {
|
|
398
470
|
"value": {"detail": self.message},
|
|
@@ -435,8 +507,8 @@ class DetailedErrorDTO(BaseModel):
|
|
|
435
507
|
error_code: Optional[str] = Field(None, description="Internal error code")
|
|
436
508
|
timestamp: Optional[str] = Field(None, description="Error timestamp")
|
|
437
509
|
|
|
438
|
-
def
|
|
439
|
-
"""Generate
|
|
510
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
511
|
+
"""Generate examples for OpenAPI."""
|
|
440
512
|
example = {"detail": self.message}
|
|
441
513
|
if self.error_code:
|
|
442
514
|
example["error_code"] = self.error_code
|
|
@@ -493,7 +565,7 @@ class DomainException(Exception):
|
|
|
493
565
|
status_code: int
|
|
494
566
|
message: str
|
|
495
567
|
|
|
496
|
-
def
|
|
568
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
497
569
|
return {self.message: {"value": {"detail": self.message}}}
|
|
498
570
|
|
|
499
571
|
@classmethod
|
|
@@ -536,7 +608,7 @@ See [examples/domain_exceptions.py](examples/domain_exceptions.py) for complete
|
|
|
536
608
|
|
|
537
609
|
## Compatibility with Existing Projects
|
|
538
610
|
|
|
539
|
-
If your project already has error DTOs (like `ApiErrorDTO`), they can work with `fastapi-errors-plus` if they implement the `ErrorDTO` protocol:
|
|
611
|
+
If your project already has error DTOs (like `ApiErrorDTO`), they can work with `fastapi-errors-plus` if they implement the `ErrorDTO` protocol (or legacy `to_example()` during migration):
|
|
540
612
|
|
|
541
613
|
```python
|
|
542
614
|
# Your existing ApiErrorDTO
|
|
@@ -545,7 +617,7 @@ class ApiErrorDTO:
|
|
|
545
617
|
status_code: int
|
|
546
618
|
message: str
|
|
547
619
|
|
|
548
|
-
def
|
|
620
|
+
def to_examples(self) -> dict:
|
|
549
621
|
return {
|
|
550
622
|
self.message: {
|
|
551
623
|
"value": {"detail": self.message},
|
|
@@ -582,6 +654,7 @@ Errors(
|
|
|
582
654
|
forbidden_403: bool = False,
|
|
583
655
|
validation_error_422: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
|
|
584
656
|
internal_server_error_500: bool = False,
|
|
657
|
+
profile: Optional[ErrorProfile] = None,
|
|
585
658
|
)
|
|
586
659
|
```
|
|
587
660
|
|
|
@@ -600,10 +673,12 @@ Errors(
|
|
|
600
673
|
- `None` (default): Add 422 (True by default, FastAPI validates all parameters)
|
|
601
674
|
- `False`: Explicitly disable 422
|
|
602
675
|
- `True`: Explicitly enable 422
|
|
603
|
-
- `internal_server_error`: Add 500 Internal Server Error (legacy,
|
|
676
|
+
- `internal_server_error`: Add 500 Internal Server Error (legacy, **deprecated** in 0.9). Defaults to `False`.
|
|
677
|
+
- `profile`: Optional **`ErrorProfile`** — project defaults; explicit kwargs override profile.
|
|
678
|
+
|
|
679
|
+
**Deprecation (0.9+):** legacy kwargs above emit `DeprecationWarning` (removal in **1.0**). Omitting both `validation_error` and `validation_error_422` still adds 422 but warns; default becomes **`False` in 1.0**.
|
|
604
680
|
|
|
605
|
-
**
|
|
606
|
-
FastAPI automatically validates all parameters (Path, Query, Body), so 422 is relevant in 95%+ of endpoints. For endpoints without parameters, explicitly set `validation_error=False` or `validation_error_422=False`.
|
|
681
|
+
**422 behaviour:** pass `validation_error_422=False` for endpoints without request-body validation or ADR-style error bodies. Use **`ErrorProfile`** to set this once per project.
|
|
607
682
|
|
|
608
683
|
**Returns:**
|
|
609
684
|
- A dict-like `Mapping[int, …]` suitable for FastAPI’s `responses` / OpenAPI
|
|
@@ -614,9 +689,13 @@ FastAPI automatically validates all parameters (Path, Query, Body), so 422 is re
|
|
|
614
689
|
```python
|
|
615
690
|
# Instances are Mapping keyed by HTTP status codes
|
|
616
691
|
error_responses = Errors(unauthorized_401=True, forbidden_403=True)
|
|
617
|
-
documented = error_responses[401] #
|
|
692
|
+
documented = error_responses[401] # deep copy — safe to read, won't mutate internal state
|
|
618
693
|
```
|
|
619
694
|
|
|
695
|
+
**Isolation (0.8+):** incoming response **dicts** are **deep-copied** on ingest; **`errors[status]`** returns a **deep copy** so callers cannot corrupt shared registry templates or internal merge state.
|
|
696
|
+
|
|
697
|
+
**Descriptions:** response blocks with only **`model`** (no `description`) get a default from **`HTTPStatus.phrase`** so OpenAPI generation does not fail.
|
|
698
|
+
|
|
620
699
|
### `ErrorDTO`
|
|
621
700
|
|
|
622
701
|
Protocol for error objects compatible with the library.
|
|
@@ -626,9 +705,47 @@ Protocol for error objects compatible with the library.
|
|
|
626
705
|
- `message: str` — Error message description
|
|
627
706
|
|
|
628
707
|
**Required methods:**
|
|
629
|
-
- `
|
|
708
|
+
- `to_examples() -> Dict[str, Any]` — OpenAPI `examples` map for `application/json`
|
|
709
|
+
|
|
710
|
+
During `Errors(...)` initialization, non-`dict` objects in `*errors` missing `status_code`, `message`, or a callable **`to_examples()`** / **`to_example()`** raise **`TypeError`** naming what was missing.
|
|
711
|
+
|
|
712
|
+
### `LegacyErrorDTO` / `ErrorDTOLike`
|
|
630
713
|
|
|
631
|
-
|
|
714
|
+
- **`LegacyErrorDTO`** — typing helper for classes that only implement deprecated **`to_example()`**.
|
|
715
|
+
- **`ErrorDTOLike`** — `Union[ErrorDTO, LegacyErrorDTO]` for transitional annotations.
|
|
716
|
+
|
|
717
|
+
### `ErrorDoc`
|
|
718
|
+
|
|
719
|
+
Declarative DTO for arbitrary example bodies and per-example **`summary`**.
|
|
720
|
+
|
|
721
|
+
**Constructor:**
|
|
722
|
+
```python
|
|
723
|
+
ErrorDoc(
|
|
724
|
+
status_code: int,
|
|
725
|
+
message: str,
|
|
726
|
+
examples: Optional[Dict[str, str | dict]] = None,
|
|
727
|
+
body: Optional[Dict[str, Any]] = None,
|
|
728
|
+
example_key: Optional[str] = None,
|
|
729
|
+
model: Any = None,
|
|
730
|
+
schema: Optional[Dict[str, Any]] = None,
|
|
731
|
+
openapi_json_extras: Optional[Dict[str, Any]] = None,
|
|
732
|
+
)
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
When **`examples`** is omitted, a single example is built from **`body`** or `{"detail": message}`.
|
|
736
|
+
|
|
737
|
+
### `ErrorProfile`
|
|
738
|
+
|
|
739
|
+
Frozen project-wide defaults for standard HTTP flags.
|
|
740
|
+
|
|
741
|
+
```python
|
|
742
|
+
ErrorProfile(
|
|
743
|
+
unauthorized_401: bool = False,
|
|
744
|
+
forbidden_403: bool = False,
|
|
745
|
+
validation_error_422: Optional[bool] = None,
|
|
746
|
+
internal_server_error_500: bool = False,
|
|
747
|
+
)
|
|
748
|
+
```
|
|
632
749
|
|
|
633
750
|
### `BaseErrorDTO`
|
|
634
751
|
|
|
@@ -639,6 +756,8 @@ Base implementation of ErrorDTO Protocol for convenience.
|
|
|
639
756
|
BaseErrorDTO(
|
|
640
757
|
status_code: int,
|
|
641
758
|
message: str,
|
|
759
|
+
model: Any = None,
|
|
760
|
+
schema: Optional[Dict[str, Any]] = None,
|
|
642
761
|
openapi_json_extras: Optional[Dict[str, Any]] = None,
|
|
643
762
|
)
|
|
644
763
|
```
|
|
@@ -767,7 +886,7 @@ class DomainException(Exception):
|
|
|
767
886
|
def __init__(self) -> None:
|
|
768
887
|
super().__init__(self.message)
|
|
769
888
|
|
|
770
|
-
def
|
|
889
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
771
890
|
return {
|
|
772
891
|
self.message: {
|
|
773
892
|
"value": {"detail": self.message},
|