fastapi-errors-plus 0.6.1__tar.gz → 0.7.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.
Files changed (19) hide show
  1. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/LICENSE +1 -0
  2. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/PKG-INFO +190 -33
  3. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/README.md +189 -32
  4. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus/__init__.py +1 -1
  5. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus/base.py +7 -0
  6. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus/errors.py +56 -8
  7. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus/protocol.py +8 -0
  8. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus.egg-info/PKG-INFO +190 -33
  9. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/pyproject.toml +15 -1
  10. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/tests/test_app.py +4 -55
  11. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/tests/test_base.py +39 -0
  12. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/tests/test_errors.py +267 -15
  13. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/tests/test_integration.py +21 -7
  14. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus/py.typed +0 -0
  15. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus.egg-info/SOURCES.txt +0 -0
  16. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus.egg-info/dependency_links.txt +0 -0
  17. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus.egg-info/requires.txt +0 -0
  18. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus.egg-info/top_level.txt +0 -0
  19. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-errors-plus
3
- Version: 0.6.1
3
+ Version: 0.7.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
@@ -30,12 +30,12 @@ Dynamic: license-file
30
30
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
31
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
32
32
  [![FastAPI](https://img.shields.io/badge/FastAPI-0.104%2B-009688.svg)](https://fastapi.tiangolo.com/)
33
- [![Tests](https://img.shields.io/badge/tests-72%20passed-success.svg)](https://github.com/seligoroff/fastapi-errors-plus)
34
- [![Coverage](https://img.shields.io/badge/coverage-83%25-green.svg)](https://github.com/seligoroff/fastapi-errors-plus)
33
+ [![Tests](https://img.shields.io/badge/tests-101-success.svg)](https://github.com/seligoroff/fastapi-errors-plus)
34
+ [![Coverage](https://img.shields.io/badge/coverage-80%25%2B-green.svg)](https://github.com/seligoroff/fastapi-errors-plus)
35
35
 
36
36
  Universal library for documenting errors in FastAPI endpoints.
37
37
 
38
- > [Русская версия README](README.ru.md)
38
+ > [Русская версия README](https://github.com/seligoroff/fastapi-errors-plus/blob/main/README.ru.md)
39
39
 
40
40
  ## Philosophy
41
41
 
@@ -207,6 +207,39 @@ def delete_item(id: int):
207
207
  pass
208
208
  ```
209
209
 
210
+ #### OpenAPI extras (`schema`) next to examples
211
+
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; **`to_example()`** still defines examples):
213
+
214
+ ```python
215
+ from fastapi import APIRouter, status
216
+ from fastapi_errors_plus import BaseErrorDTO, Errors
217
+
218
+ ADR_ERROR_BODY_SCHEMA = {
219
+ "type": "object",
220
+ "required": ["code", "detail"],
221
+ "properties": {
222
+ "code": {"type": "string"},
223
+ "detail": {"type": "string"},
224
+ "context": {"type": "object"},
225
+ },
226
+ }
227
+
228
+ business_conflict = BaseErrorDTO(
229
+ status_code=status.HTTP_409_CONFLICT,
230
+ message="BusinessRuleViolation",
231
+ openapi_json_extras={"schema": ADR_ERROR_BODY_SCHEMA},
232
+ )
233
+
234
+ router = APIRouter()
235
+
236
+ @router.post("/items", responses=Errors(business_conflict, validation_error=False))
237
+ def create_item():
238
+ ...
239
+ ```
240
+
241
+ Custom **`ErrorDTO`** classes may instead define **`to_openapi_json_media_type_extras()`** returning a **`dict`**; if present and non-empty, it overrides **`openapi_json_extras`**. Any later **`dict`** in **`Errors`** for that status still overrides the same **`application/json`** keys (same precedence as merging two dicts).
242
+
210
243
  #### StandardErrorDTO
211
244
 
212
245
  Extended implementation for errors with multiple examples (useful for standard HTTP errors):
@@ -297,6 +330,37 @@ def update_item(id: int):
297
330
 
298
331
  The OpenAPI spec will contain both examples under the 404 status code.
299
332
 
333
+ When merging the same status code: a **dict** wins for **`description`** over the bundled standard-flag wording; an **ErrorDTO**’s `message` can replace the description only while it still matches the library’s default label for that code (custom descriptions coming from dicts are not overwritten by DTOs).
334
+
335
+ Under `content["application/json"]`, `example` / `examples` are merged as before; any **other** OpenAPI Media Type fields from a later **`dict`** (for example **`schema`**, **`encoding`**) are **copied in** as well—the later dict wins on conflict (**same rule as `description`**).
336
+
337
+ You can combine one **`ErrorDTO`** (examples) with a **`dict`** for the **same numeric status code** listing only **`schema`** (or other non-example keys) without repeating the boilerplate examples block:
338
+
339
+ ```python
340
+ Errors(
341
+ conflict_error_doc, # implements ErrorDTO, e.g. .for_openapi() for ADR-shaped examples
342
+ {
343
+ status.HTTP_409_CONFLICT: {
344
+ "description": "Business rule violation",
345
+ "content": {
346
+ "application/json": {
347
+ "schema": {
348
+ "type": "object",
349
+ "properties": {
350
+ "code": {"type": "string"},
351
+ "detail": {"type": "string"},
352
+ "context": {"type": "object"},
353
+ },
354
+ },
355
+ },
356
+ },
357
+ },
358
+ },
359
+ )
360
+ ```
361
+
362
+ Order matters only for overlaps: whichever **`dict`** is applied **later** in the **`Errors`** argument list overwrites **`schema`** / **`encoding`** (and **`model`** on the outer response dict, if provided) when the same keys appear again.
363
+
300
364
  ## ErrorDTO Protocol
301
365
 
302
366
  The `ErrorDTO` protocol defines the interface for error objects compatible with the library:
@@ -335,6 +399,102 @@ Any class implementing this protocol (through structural typing) can be used wit
335
399
 
336
400
  Both approaches work together — you can mix them in the same `Errors()` call!
337
401
 
402
+ ## Using Pydantic with ErrorDTO
403
+
404
+ **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
+
406
+ Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_example()`) will work, including Pydantic models.
407
+
408
+ ### Simple Pydantic Model as ErrorDTO
409
+
410
+ ```python
411
+ from pydantic import BaseModel, Field
412
+ from fastapi_errors_plus import Errors
413
+ from typing import Dict, Any
414
+
415
+ class PydanticErrorDTO(BaseModel):
416
+ """Pydantic model implementing ErrorDTO Protocol."""
417
+ status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
418
+ message: str = Field(..., min_length=1, description="Error message")
419
+
420
+ def to_example(self) -> Dict[str, Any]:
421
+ """Generate example for OpenAPI."""
422
+ return {
423
+ self.message: {
424
+ "value": {"detail": self.message},
425
+ },
426
+ }
427
+
428
+ # Usage
429
+ notification_error = PydanticErrorDTO(
430
+ status_code=404,
431
+ message="Notification not found",
432
+ )
433
+
434
+ @router.delete(
435
+ "/{id}",
436
+ responses=Errors(notification_error),
437
+ )
438
+ def delete_item(id: int):
439
+ pass
440
+ ```
441
+
442
+ **Benefits:**
443
+ - Runtime validation through Pydantic
444
+ - Type safety
445
+ - Automatic field documentation
446
+ - Works with ErrorDTO Protocol through structural typing
447
+
448
+ ### Complex ErrorDTO with Pydantic
449
+
450
+ For errors with additional fields:
451
+
452
+ ```python
453
+ from pydantic import BaseModel, Field
454
+ from fastapi_errors_plus import Errors
455
+ from typing import Dict, Any, Optional
456
+
457
+ class DetailedErrorDTO(BaseModel):
458
+ """Pydantic model for errors with additional fields."""
459
+ status_code: int = Field(..., ge=400, le=599)
460
+ message: str = Field(..., min_length=1)
461
+ error_code: Optional[str] = Field(None, description="Internal error code")
462
+ timestamp: Optional[str] = Field(None, description="Error timestamp")
463
+
464
+ def to_example(self) -> Dict[str, Any]:
465
+ """Generate example for OpenAPI."""
466
+ example = {"detail": self.message}
467
+ if self.error_code:
468
+ example["error_code"] = self.error_code
469
+ if self.timestamp:
470
+ example["timestamp"] = self.timestamp
471
+
472
+ return {
473
+ self.message: {
474
+ "value": example,
475
+ },
476
+ }
477
+
478
+ # Usage
479
+ validation_error = DetailedErrorDTO(
480
+ status_code=422,
481
+ message="Validation failed",
482
+ error_code="VALIDATION_ERROR",
483
+ timestamp="2025-01-15T10:30:00Z",
484
+ )
485
+ ```
486
+
487
+ **When to use Pydantic with ErrorDTO:**
488
+ - Your project already uses Pydantic extensively
489
+ - You need runtime validation for error objects
490
+ - You want automatic field documentation
491
+ - You have complex error structures with multiple fields
492
+
493
+ **When not to use Pydantic:**
494
+ - Your project doesn't use Pydantic (use `BaseErrorDTO` or `StandardErrorDTO` instead)
495
+ - You need simple error objects (dataclasses are sufficient)
496
+ - You want to keep dependencies minimal
497
+
338
498
  ## Best Practice: Connecting Exceptions and ErrorDTO
339
499
 
340
500
  ### Problem
@@ -472,14 +632,15 @@ Errors(
472
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`.
473
633
 
474
634
  **Returns:**
475
- - Mapping object (dict-like) that implements `Dict[int, Dict[str, Any]]` for FastAPI responses
476
- - Can be used directly in `responses` parameter without calling: `responses=Errors(...)`
635
+ - A dict-like `Mapping[int, ]` suitable for FastAPI’s `responses` / OpenAPI
636
+ - Pass the instance as-is **do not** call it like a function: `responses=Errors(...)`
477
637
 
478
638
  #### Usage
479
639
 
480
640
  ```python
481
- # Call the instance to get responses dict
482
- responses = Errors(unauthorized=True)
641
+ # Instances are Mapping keyed by HTTP status codes
642
+ error_responses = Errors(unauthorized_401=True, forbidden_403=True)
643
+ documented = error_responses[401] # response block picked up by OpenAPI
483
644
  ```
484
645
 
485
646
  ### `ErrorDTO`
@@ -493,6 +654,8 @@ Protocol for error objects compatible with the library.
493
654
  **Required methods:**
494
655
  - `to_example() -> Dict[str, Any]` — Generate example for OpenAPI
495
656
 
657
+ During `Errors(...)` initialization, non-`dict` objects in `*errors` missing `status_code`, `message`, or a callable `to_example` raise **`TypeError`** naming what was missing.
658
+
496
659
  ### `BaseErrorDTO`
497
660
 
498
661
  Base implementation of ErrorDTO Protocol for convenience.
@@ -502,6 +665,7 @@ Base implementation of ErrorDTO Protocol for convenience.
502
665
  BaseErrorDTO(
503
666
  status_code: int,
504
667
  message: str,
668
+ openapi_json_extras: Optional[Dict[str, Any]] = None,
505
669
  )
506
670
  ```
507
671
 
@@ -519,6 +683,7 @@ Extended implementation for errors with multiple examples.
519
683
  StandardErrorDTO(
520
684
  status_code: int,
521
685
  message: str,
686
+ openapi_json_extras: Optional[Dict[str, Any]] = None,
522
687
  examples: Optional[Dict[str, str]] = None,
523
688
  )
524
689
  ```
@@ -570,7 +735,7 @@ def delete_item(id: int):
570
735
  ```python
571
736
  from fastapi import APIRouter
572
737
  from fastapi_errors_plus import Errors
573
- from api.exceptions.dto import errors # Your project's error DTOs
738
+ from api.exceptions.dto import notification_not_found_error # ErrorDTO-compatible instance
574
739
 
575
740
  router = APIRouter()
576
741
 
@@ -579,7 +744,7 @@ router = APIRouter()
579
744
  responses=Errors(
580
745
  unauthorized=True,
581
746
  forbidden=True,
582
- errors.notification_not_found_error, # Your ErrorDTO
747
+ notification_not_found_error,
583
748
  ),
584
749
  )
585
750
  async def delete_notification(notification_id: int):
@@ -618,40 +783,32 @@ This example shows how to use `fastapi-errors-plus` in a FastAPI project with Cl
618
783
 
619
784
  **Domain Layer** (`domain/errors.py`):
620
785
  ```python
621
- from typing import Protocol, Dict, Any
786
+ from typing import Dict, Any
622
787
 
623
- class DomainError(Protocol):
624
- """Domain error protocol compatible with ErrorDTO."""
788
+ class DomainException(Exception):
789
+ """Domain exception usable as runtime error and shaped like ErrorDTO for OpenAPI."""
625
790
  status_code: int
626
791
  message: str
627
-
628
- def to_example(self) -> Dict[str, Any]:
629
- """Generate example for OpenAPI."""
630
- ...
631
792
 
632
- class ItemNotFoundError:
633
- """Domain error for item not found."""
634
- status_code = 404
635
- message = "Item not found"
636
-
793
+ def __init__(self) -> None:
794
+ super().__init__(self.message)
795
+
637
796
  def to_example(self) -> Dict[str, Any]:
638
797
  return {
639
- "ItemNotFound": {
640
- "value": {"detail": "Item not found"},
798
+ self.message: {
799
+ "value": {"detail": self.message},
641
800
  },
642
801
  }
643
802
 
644
- class ItemAlreadyExistsError:
645
- """Domain error for item already exists."""
803
+
804
+ class ItemNotFoundError(DomainException):
805
+ status_code = 404
806
+ message = "Item not found"
807
+
808
+
809
+ class ItemAlreadyExistsError(DomainException):
646
810
  status_code = 409
647
811
  message = "Item already exists"
648
-
649
- def to_example(self) -> Dict[str, Any]:
650
- return {
651
- "ItemAlreadyExists": {
652
- "value": {"detail": "Item already exists"},
653
- },
654
- }
655
812
  ```
656
813
 
657
814
  **Application Layer** (`application/use_cases.py`):
@@ -4,12 +4,12 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
6
6
  [![FastAPI](https://img.shields.io/badge/FastAPI-0.104%2B-009688.svg)](https://fastapi.tiangolo.com/)
7
- [![Tests](https://img.shields.io/badge/tests-72%20passed-success.svg)](https://github.com/seligoroff/fastapi-errors-plus)
8
- [![Coverage](https://img.shields.io/badge/coverage-83%25-green.svg)](https://github.com/seligoroff/fastapi-errors-plus)
7
+ [![Tests](https://img.shields.io/badge/tests-101-success.svg)](https://github.com/seligoroff/fastapi-errors-plus)
8
+ [![Coverage](https://img.shields.io/badge/coverage-80%25%2B-green.svg)](https://github.com/seligoroff/fastapi-errors-plus)
9
9
 
10
10
  Universal library for documenting errors in FastAPI endpoints.
11
11
 
12
- > [Русская версия README](README.ru.md)
12
+ > [Русская версия README](https://github.com/seligoroff/fastapi-errors-plus/blob/main/README.ru.md)
13
13
 
14
14
  ## Philosophy
15
15
 
@@ -181,6 +181,39 @@ def delete_item(id: int):
181
181
  pass
182
182
  ```
183
183
 
184
+ #### OpenAPI extras (`schema`) next to examples
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; **`to_example()`** still defines examples):
187
+
188
+ ```python
189
+ from fastapi import APIRouter, status
190
+ from fastapi_errors_plus import BaseErrorDTO, Errors
191
+
192
+ ADR_ERROR_BODY_SCHEMA = {
193
+ "type": "object",
194
+ "required": ["code", "detail"],
195
+ "properties": {
196
+ "code": {"type": "string"},
197
+ "detail": {"type": "string"},
198
+ "context": {"type": "object"},
199
+ },
200
+ }
201
+
202
+ business_conflict = BaseErrorDTO(
203
+ status_code=status.HTTP_409_CONFLICT,
204
+ message="BusinessRuleViolation",
205
+ openapi_json_extras={"schema": ADR_ERROR_BODY_SCHEMA},
206
+ )
207
+
208
+ router = APIRouter()
209
+
210
+ @router.post("/items", responses=Errors(business_conflict, validation_error=False))
211
+ def create_item():
212
+ ...
213
+ ```
214
+
215
+ Custom **`ErrorDTO`** classes may instead define **`to_openapi_json_media_type_extras()`** returning a **`dict`**; if present and non-empty, it overrides **`openapi_json_extras`**. Any later **`dict`** in **`Errors`** for that status still overrides the same **`application/json`** keys (same precedence as merging two dicts).
216
+
184
217
  #### StandardErrorDTO
185
218
 
186
219
  Extended implementation for errors with multiple examples (useful for standard HTTP errors):
@@ -271,6 +304,37 @@ def update_item(id: int):
271
304
 
272
305
  The OpenAPI spec will contain both examples under the 404 status code.
273
306
 
307
+ When merging the same status code: a **dict** wins for **`description`** over the bundled standard-flag wording; an **ErrorDTO**’s `message` can replace the description only while it still matches the library’s default label for that code (custom descriptions coming from dicts are not overwritten by DTOs).
308
+
309
+ Under `content["application/json"]`, `example` / `examples` are merged as before; any **other** OpenAPI Media Type fields from a later **`dict`** (for example **`schema`**, **`encoding`**) are **copied in** as well—the later dict wins on conflict (**same rule as `description`**).
310
+
311
+ You can combine one **`ErrorDTO`** (examples) with a **`dict`** for the **same numeric status code** listing only **`schema`** (or other non-example keys) without repeating the boilerplate examples block:
312
+
313
+ ```python
314
+ Errors(
315
+ conflict_error_doc, # implements ErrorDTO, e.g. .for_openapi() for ADR-shaped examples
316
+ {
317
+ status.HTTP_409_CONFLICT: {
318
+ "description": "Business rule violation",
319
+ "content": {
320
+ "application/json": {
321
+ "schema": {
322
+ "type": "object",
323
+ "properties": {
324
+ "code": {"type": "string"},
325
+ "detail": {"type": "string"},
326
+ "context": {"type": "object"},
327
+ },
328
+ },
329
+ },
330
+ },
331
+ },
332
+ },
333
+ )
334
+ ```
335
+
336
+ Order matters only for overlaps: whichever **`dict`** is applied **later** in the **`Errors`** argument list overwrites **`schema`** / **`encoding`** (and **`model`** on the outer response dict, if provided) when the same keys appear again.
337
+
274
338
  ## ErrorDTO Protocol
275
339
 
276
340
  The `ErrorDTO` protocol defines the interface for error objects compatible with the library:
@@ -309,6 +373,102 @@ Any class implementing this protocol (through structural typing) can be used wit
309
373
 
310
374
  Both approaches work together — you can mix them in the same `Errors()` call!
311
375
 
376
+ ## Using Pydantic with ErrorDTO
377
+
378
+ **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
+
380
+ Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_example()`) will work, including Pydantic models.
381
+
382
+ ### Simple Pydantic Model as ErrorDTO
383
+
384
+ ```python
385
+ from pydantic import BaseModel, Field
386
+ from fastapi_errors_plus import Errors
387
+ from typing import Dict, Any
388
+
389
+ class PydanticErrorDTO(BaseModel):
390
+ """Pydantic model implementing ErrorDTO Protocol."""
391
+ status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
392
+ message: str = Field(..., min_length=1, description="Error message")
393
+
394
+ def to_example(self) -> Dict[str, Any]:
395
+ """Generate example for OpenAPI."""
396
+ return {
397
+ self.message: {
398
+ "value": {"detail": self.message},
399
+ },
400
+ }
401
+
402
+ # Usage
403
+ notification_error = PydanticErrorDTO(
404
+ status_code=404,
405
+ message="Notification not found",
406
+ )
407
+
408
+ @router.delete(
409
+ "/{id}",
410
+ responses=Errors(notification_error),
411
+ )
412
+ def delete_item(id: int):
413
+ pass
414
+ ```
415
+
416
+ **Benefits:**
417
+ - Runtime validation through Pydantic
418
+ - Type safety
419
+ - Automatic field documentation
420
+ - Works with ErrorDTO Protocol through structural typing
421
+
422
+ ### Complex ErrorDTO with Pydantic
423
+
424
+ For errors with additional fields:
425
+
426
+ ```python
427
+ from pydantic import BaseModel, Field
428
+ from fastapi_errors_plus import Errors
429
+ from typing import Dict, Any, Optional
430
+
431
+ class DetailedErrorDTO(BaseModel):
432
+ """Pydantic model for errors with additional fields."""
433
+ status_code: int = Field(..., ge=400, le=599)
434
+ message: str = Field(..., min_length=1)
435
+ error_code: Optional[str] = Field(None, description="Internal error code")
436
+ timestamp: Optional[str] = Field(None, description="Error timestamp")
437
+
438
+ def to_example(self) -> Dict[str, Any]:
439
+ """Generate example for OpenAPI."""
440
+ example = {"detail": self.message}
441
+ if self.error_code:
442
+ example["error_code"] = self.error_code
443
+ if self.timestamp:
444
+ example["timestamp"] = self.timestamp
445
+
446
+ return {
447
+ self.message: {
448
+ "value": example,
449
+ },
450
+ }
451
+
452
+ # Usage
453
+ validation_error = DetailedErrorDTO(
454
+ status_code=422,
455
+ message="Validation failed",
456
+ error_code="VALIDATION_ERROR",
457
+ timestamp="2025-01-15T10:30:00Z",
458
+ )
459
+ ```
460
+
461
+ **When to use Pydantic with ErrorDTO:**
462
+ - Your project already uses Pydantic extensively
463
+ - You need runtime validation for error objects
464
+ - You want automatic field documentation
465
+ - You have complex error structures with multiple fields
466
+
467
+ **When not to use Pydantic:**
468
+ - Your project doesn't use Pydantic (use `BaseErrorDTO` or `StandardErrorDTO` instead)
469
+ - You need simple error objects (dataclasses are sufficient)
470
+ - You want to keep dependencies minimal
471
+
312
472
  ## Best Practice: Connecting Exceptions and ErrorDTO
313
473
 
314
474
  ### Problem
@@ -446,14 +606,15 @@ Errors(
446
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`.
447
607
 
448
608
  **Returns:**
449
- - Mapping object (dict-like) that implements `Dict[int, Dict[str, Any]]` for FastAPI responses
450
- - Can be used directly in `responses` parameter without calling: `responses=Errors(...)`
609
+ - A dict-like `Mapping[int, ]` suitable for FastAPI’s `responses` / OpenAPI
610
+ - Pass the instance as-is **do not** call it like a function: `responses=Errors(...)`
451
611
 
452
612
  #### Usage
453
613
 
454
614
  ```python
455
- # Call the instance to get responses dict
456
- responses = Errors(unauthorized=True)
615
+ # Instances are Mapping keyed by HTTP status codes
616
+ error_responses = Errors(unauthorized_401=True, forbidden_403=True)
617
+ documented = error_responses[401] # response block picked up by OpenAPI
457
618
  ```
458
619
 
459
620
  ### `ErrorDTO`
@@ -467,6 +628,8 @@ Protocol for error objects compatible with the library.
467
628
  **Required methods:**
468
629
  - `to_example() -> Dict[str, Any]` — Generate example for OpenAPI
469
630
 
631
+ During `Errors(...)` initialization, non-`dict` objects in `*errors` missing `status_code`, `message`, or a callable `to_example` raise **`TypeError`** naming what was missing.
632
+
470
633
  ### `BaseErrorDTO`
471
634
 
472
635
  Base implementation of ErrorDTO Protocol for convenience.
@@ -476,6 +639,7 @@ Base implementation of ErrorDTO Protocol for convenience.
476
639
  BaseErrorDTO(
477
640
  status_code: int,
478
641
  message: str,
642
+ openapi_json_extras: Optional[Dict[str, Any]] = None,
479
643
  )
480
644
  ```
481
645
 
@@ -493,6 +657,7 @@ Extended implementation for errors with multiple examples.
493
657
  StandardErrorDTO(
494
658
  status_code: int,
495
659
  message: str,
660
+ openapi_json_extras: Optional[Dict[str, Any]] = None,
496
661
  examples: Optional[Dict[str, str]] = None,
497
662
  )
498
663
  ```
@@ -544,7 +709,7 @@ def delete_item(id: int):
544
709
  ```python
545
710
  from fastapi import APIRouter
546
711
  from fastapi_errors_plus import Errors
547
- from api.exceptions.dto import errors # Your project's error DTOs
712
+ from api.exceptions.dto import notification_not_found_error # ErrorDTO-compatible instance
548
713
 
549
714
  router = APIRouter()
550
715
 
@@ -553,7 +718,7 @@ router = APIRouter()
553
718
  responses=Errors(
554
719
  unauthorized=True,
555
720
  forbidden=True,
556
- errors.notification_not_found_error, # Your ErrorDTO
721
+ notification_not_found_error,
557
722
  ),
558
723
  )
559
724
  async def delete_notification(notification_id: int):
@@ -592,40 +757,32 @@ This example shows how to use `fastapi-errors-plus` in a FastAPI project with Cl
592
757
 
593
758
  **Domain Layer** (`domain/errors.py`):
594
759
  ```python
595
- from typing import Protocol, Dict, Any
760
+ from typing import Dict, Any
596
761
 
597
- class DomainError(Protocol):
598
- """Domain error protocol compatible with ErrorDTO."""
762
+ class DomainException(Exception):
763
+ """Domain exception usable as runtime error and shaped like ErrorDTO for OpenAPI."""
599
764
  status_code: int
600
765
  message: str
601
-
602
- def to_example(self) -> Dict[str, Any]:
603
- """Generate example for OpenAPI."""
604
- ...
605
766
 
606
- class ItemNotFoundError:
607
- """Domain error for item not found."""
608
- status_code = 404
609
- message = "Item not found"
610
-
767
+ def __init__(self) -> None:
768
+ super().__init__(self.message)
769
+
611
770
  def to_example(self) -> Dict[str, Any]:
612
771
  return {
613
- "ItemNotFound": {
614
- "value": {"detail": "Item not found"},
772
+ self.message: {
773
+ "value": {"detail": self.message},
615
774
  },
616
775
  }
617
776
 
618
- class ItemAlreadyExistsError:
619
- """Domain error for item already exists."""
777
+
778
+ class ItemNotFoundError(DomainException):
779
+ status_code = 404
780
+ message = "Item not found"
781
+
782
+
783
+ class ItemAlreadyExistsError(DomainException):
620
784
  status_code = 409
621
785
  message = "Item already exists"
622
-
623
- def to_example(self) -> Dict[str, Any]:
624
- return {
625
- "ItemAlreadyExists": {
626
- "value": {"detail": "Item already exists"},
627
- },
628
- }
629
786
  ```
630
787
 
631
788
  **Application Layer** (`application/use_cases.py`):
@@ -10,5 +10,5 @@ __all__ = [
10
10
  "BaseErrorDTO", # Base implementation (optional)
11
11
  "StandardErrorDTO", # Extended implementation (optional)
12
12
  ]
13
- __version__ = "0.6.1"
13
+ __version__ = "0.7.0"
14
14
 
@@ -37,6 +37,12 @@ class BaseErrorDTO:
37
37
  message: str
38
38
  """Error message description."""
39
39
 
40
+ openapi_json_extras: Optional[Dict[str, Any]] = field(default=None)
41
+ """Optional OpenAPI fragment under ``content['application/json']`` besides examples,
42
+ typically ``{\"schema\": ...}`` or ``encoding``. Do **not** put ``example`` / ``examples``
43
+ here — use :meth:`to_example`. Arbitrary implementations may instead implement
44
+ :meth:`to_openapi_json_media_type_extras`; that return value wins over this attribute."""
45
+
40
46
  def to_example(self) -> Dict[str, Any]:
41
47
  """Generate example for OpenAPI.
42
48
 
@@ -122,3 +128,4 @@ class StandardErrorDTO(BaseErrorDTO):
122
128
 
123
129
 
124
130
 
131
+