fastapi-errors-plus 0.4.0__tar.gz → 0.5.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.4.0 → fastapi_errors_plus-0.5.0}/LICENSE +1 -0
  2. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/PKG-INFO +13 -6
  3. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/README.md +12 -5
  4. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/fastapi_errors_plus/__init__.py +1 -1
  5. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/fastapi_errors_plus/base.py +1 -0
  6. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/fastapi_errors_plus/errors.py +57 -11
  7. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/fastapi_errors_plus/protocol.py +1 -0
  8. fastapi_errors_plus-0.5.0/fastapi_errors_plus/py.typed +0 -0
  9. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/fastapi_errors_plus.egg-info/PKG-INFO +13 -6
  10. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/fastapi_errors_plus.egg-info/SOURCES.txt +1 -0
  11. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/pyproject.toml +1 -1
  12. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/tests/test_base.py +1 -0
  13. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/tests/test_errors.py +123 -0
  14. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/fastapi_errors_plus.egg-info/dependency_links.txt +0 -0
  15. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/fastapi_errors_plus.egg-info/requires.txt +0 -0
  16. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/fastapi_errors_plus.egg-info/top_level.txt +0 -0
  17. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/setup.cfg +0 -0
  18. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/tests/test_app.py +0 -0
  19. {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.5.0}/tests/test_integration.py +0 -0
@@ -21,3 +21,4 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
22
 
23
23
 
24
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-errors-plus
3
- Version: 0.4.0
3
+ Version: 0.5.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
@@ -375,11 +375,11 @@ Errors(
375
375
  *errors: Union[Dict[int, Dict[str, Any]], ErrorDTO],
376
376
  unauthorized: bool = False,
377
377
  forbidden: bool = False,
378
- validation_error: bool = True, # Defaults to True (FastAPI validates all parameters)
378
+ validation_error: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
379
379
  internal_server_error: bool = False,
380
380
  unauthorized_401: bool = False,
381
381
  forbidden_403: bool = False,
382
- validation_error_422: bool = True, # Defaults to True (FastAPI validates all parameters)
382
+ validation_error_422: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
383
383
  internal_server_error_500: bool = False,
384
384
  )
385
385
  ```
@@ -388,18 +388,25 @@ Errors(
388
388
  - `*errors`: Arbitrary errors as dict or ErrorDTO objects
389
389
  - `unauthorized_401`: Add 401 Unauthorized error (recommended, explicit). Defaults to `False`.
390
390
  - `forbidden_403`: Add 403 Forbidden error (recommended, explicit). Defaults to `False`.
391
- - `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit). Defaults to `True` (FastAPI validates all parameters).
391
+ - `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit).
392
+ - `None` (default): Add 422 (True by default, FastAPI validates all parameters)
393
+ - `False`: Explicitly disable 422
394
+ - `True`: Explicitly enable 422
392
395
  - `internal_server_error_500`: Add 500 Internal Server Error (recommended, explicit). Defaults to `False`.
393
396
  - `unauthorized`: Add 401 Unauthorized error (legacy, for backward compatibility). Defaults to `False`.
394
397
  - `forbidden`: Add 403 Forbidden error (legacy, for backward compatibility). Defaults to `False`.
395
- - `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility). Defaults to `True` (FastAPI validates all parameters).
398
+ - `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility).
399
+ - `None` (default): Add 422 (True by default, FastAPI validates all parameters)
400
+ - `False`: Explicitly disable 422
401
+ - `True`: Explicitly enable 422
396
402
  - `internal_server_error`: Add 500 Internal Server Error (legacy, for backward compatibility). Defaults to `False`.
397
403
 
398
404
  **Why `validation_error=True` by default?**
399
405
  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`.
400
406
 
401
407
  **Returns:**
402
- - Callable object that returns `Dict[int, Dict[str, Any]]` in FastAPI responses format
408
+ - Mapping object (dict-like) that implements `Dict[int, Dict[str, Any]]` for FastAPI responses
409
+ - Can be used directly in `responses` parameter without calling: `responses=Errors(...)`
403
410
 
404
411
  #### Usage
405
412
 
@@ -349,11 +349,11 @@ Errors(
349
349
  *errors: Union[Dict[int, Dict[str, Any]], ErrorDTO],
350
350
  unauthorized: bool = False,
351
351
  forbidden: bool = False,
352
- validation_error: bool = True, # Defaults to True (FastAPI validates all parameters)
352
+ validation_error: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
353
353
  internal_server_error: bool = False,
354
354
  unauthorized_401: bool = False,
355
355
  forbidden_403: bool = False,
356
- validation_error_422: bool = True, # Defaults to True (FastAPI validates all parameters)
356
+ validation_error_422: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
357
357
  internal_server_error_500: bool = False,
358
358
  )
359
359
  ```
@@ -362,18 +362,25 @@ Errors(
362
362
  - `*errors`: Arbitrary errors as dict or ErrorDTO objects
363
363
  - `unauthorized_401`: Add 401 Unauthorized error (recommended, explicit). Defaults to `False`.
364
364
  - `forbidden_403`: Add 403 Forbidden error (recommended, explicit). Defaults to `False`.
365
- - `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit). Defaults to `True` (FastAPI validates all parameters).
365
+ - `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit).
366
+ - `None` (default): Add 422 (True by default, FastAPI validates all parameters)
367
+ - `False`: Explicitly disable 422
368
+ - `True`: Explicitly enable 422
366
369
  - `internal_server_error_500`: Add 500 Internal Server Error (recommended, explicit). Defaults to `False`.
367
370
  - `unauthorized`: Add 401 Unauthorized error (legacy, for backward compatibility). Defaults to `False`.
368
371
  - `forbidden`: Add 403 Forbidden error (legacy, for backward compatibility). Defaults to `False`.
369
- - `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility). Defaults to `True` (FastAPI validates all parameters).
372
+ - `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility).
373
+ - `None` (default): Add 422 (True by default, FastAPI validates all parameters)
374
+ - `False`: Explicitly disable 422
375
+ - `True`: Explicitly enable 422
370
376
  - `internal_server_error`: Add 500 Internal Server Error (legacy, for backward compatibility). Defaults to `False`.
371
377
 
372
378
  **Why `validation_error=True` by default?**
373
379
  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`.
374
380
 
375
381
  **Returns:**
376
- - Callable object that returns `Dict[int, Dict[str, Any]]` in FastAPI responses format
382
+ - Mapping object (dict-like) that implements `Dict[int, Dict[str, Any]]` for FastAPI responses
383
+ - Can be used directly in `responses` parameter without calling: `responses=Errors(...)`
377
384
 
378
385
  #### Usage
379
386
 
@@ -10,5 +10,5 @@ __all__ = [
10
10
  "BaseErrorDTO", # Base implementation (optional)
11
11
  "StandardErrorDTO", # Extended implementation (optional)
12
12
  ]
13
- __version__ = "0.4.0"
13
+ __version__ = "0.5.0"
14
14
 
@@ -120,3 +120,4 @@ class StandardErrorDTO(BaseErrorDTO):
120
120
  for key, message in self.examples.items()
121
121
  }
122
122
 
123
+
@@ -1,7 +1,7 @@
1
1
  """Main Errors class for documenting errors in FastAPI endpoints."""
2
2
 
3
3
  from collections.abc import Mapping
4
- from typing import Any, Dict, Iterator, Union
4
+ from typing import Any, Dict, Iterator, Optional, Union
5
5
 
6
6
  from fastapi import status
7
7
 
@@ -29,10 +29,7 @@ class Errors(Mapping):
29
29
  @router.delete(
30
30
  "/{id}",
31
31
  responses=Errors(
32
- unauthorized_401=True, # 401 (explicit)
33
- forbidden_403=True, # 403 (explicit)
34
- validation_error_422=True, # 422 (explicit)
35
- {404: { # 404 via dict
32
+ {404: { # 404 via dict (positional first)
36
33
  "description": "Not found",
37
34
  "content": {
38
35
  "application/json": {
@@ -40,6 +37,9 @@ class Errors(Mapping):
40
37
  },
41
38
  },
42
39
  }},
40
+ unauthorized_401=True, # 401 (explicit, named after positional)
41
+ forbidden_403=True, # 403 (explicit)
42
+ # validation_error_422=True - not needed, defaults to True
43
43
  ),
44
44
  )
45
45
  def delete_item(id: int):
@@ -55,12 +55,12 @@ class Errors(Mapping):
55
55
  # Old parameters (for backward compatibility)
56
56
  unauthorized: bool = False, # 401
57
57
  forbidden: bool = False, # 403
58
- validation_error: bool = None, # 422 (None = use default True, False = disable, True = enable)
58
+ validation_error: Optional[bool] = None, # 422 (None = use default True, False = disable, True = enable)
59
59
  internal_server_error: bool = False, # 500
60
60
  # New parameters with explicit status codes (recommended)
61
61
  unauthorized_401: bool = False, # 401 (explicit)
62
62
  forbidden_403: bool = False, # 403 (explicit)
63
- validation_error_422: bool = None, # 422 (explicit, None = use default True, False = disable, True = enable)
63
+ validation_error_422: Optional[bool] = None, # 422 (explicit, None = use default True, False = disable, True = enable)
64
64
  internal_server_error_500: bool = False, # 500 (explicit)
65
65
  ) -> None:
66
66
  """Initialize Errors instance.
@@ -73,7 +73,10 @@ class Errors(Mapping):
73
73
  Deprecated: Use `unauthorized_401` instead for explicit status code.
74
74
  forbidden: Add 403 Forbidden error. Defaults to False.
75
75
  Deprecated: Use `forbidden_403` instead for explicit status code.
76
- validation_error: Add 422 Unprocessable Entity error. Defaults to True (None means True).
76
+ validation_error: Add 422 Unprocessable Entity error.
77
+ - None (default): Add 422 (True by default, FastAPI validates all parameters)
78
+ - False: Explicitly disable 422
79
+ - True: Explicitly enable 422
77
80
  FastAPI automatically validates all parameters (Path, Query, Body), so 422 is relevant
78
81
  in 95%+ of endpoints. Set to False only for endpoints without parameters.
79
82
  Deprecated: Use `validation_error_422` instead for explicit status code.
@@ -81,7 +84,10 @@ class Errors(Mapping):
81
84
  Deprecated: Use `internal_server_error_500` instead for explicit status code.
82
85
  unauthorized_401: Add 401 Unauthorized error (explicit). Defaults to False.
83
86
  forbidden_403: Add 403 Forbidden error (explicit). Defaults to False.
84
- validation_error_422: Add 422 Unprocessable Entity error (explicit). Defaults to True (None means True).
87
+ validation_error_422: Add 422 Unprocessable Entity error (explicit).
88
+ - None (default): Add 422 (True by default, FastAPI validates all parameters)
89
+ - False: Explicitly disable 422
90
+ - True: Explicitly enable 422
85
91
  FastAPI automatically validates all parameters (Path, Query, Body), so 422 is relevant
86
92
  in 95%+ of endpoints. Set to False only for endpoints without parameters.
87
93
  internal_server_error_500: Add 500 Internal Server Error (explicit). Defaults to False.
@@ -205,6 +211,38 @@ class Errors(Mapping):
205
211
  f"Got {type(error).__name__}"
206
212
  )
207
213
 
214
+ def _unique_key(self, examples: Dict[str, Any], base: str) -> str:
215
+ """Generate unique key for examples dict.
216
+
217
+ Args:
218
+ examples: Existing examples dict
219
+ base: Base key name
220
+
221
+ Returns:
222
+ Unique key that doesn't exist in examples
223
+
224
+ Example:
225
+ >>> errors = Errors()
226
+ >>> errors._unique_key({"default": {}}, "default")
227
+ "default_2"
228
+ >>> errors._unique_key({"default": {}, "default_2": {}}, "default")
229
+ "default_3"
230
+ """
231
+ key = base
232
+ i = 2
233
+ while key in examples:
234
+ key = f"{base}_{i}"
235
+ i += 1
236
+ return key
237
+
238
+ # Standard descriptions for priority checking
239
+ STANDARD_DESCRIPTIONS = {
240
+ status.HTTP_401_UNAUTHORIZED: "Unauthorized",
241
+ status.HTTP_403_FORBIDDEN: "Forbidden",
242
+ status.HTTP_422_UNPROCESSABLE_ENTITY: "Validation Error",
243
+ status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal Server Error",
244
+ }
245
+
208
246
  def _add_standard_error(
209
247
  self,
210
248
  status_code: int,
@@ -257,7 +295,9 @@ class Errors(Mapping):
257
295
  content["examples"] = {}
258
296
 
259
297
  # Add new example with unique key (don't overwrite existing!)
260
- content["examples"][example_key] = {"value": example}
298
+ # Check if example_key already exists in examples, use unique key if needed
299
+ unique_key = self._unique_key(content["examples"], example_key)
300
+ content["examples"][unique_key] = {"value": example}
261
301
 
262
302
  # Don't overwrite description if it already exists
263
303
  # Priority: dict > DTO > standard flags
@@ -334,7 +374,8 @@ class Errors(Mapping):
334
374
  existing_json["examples"]["default"] = {"value": response_json["example"]}
335
375
  else:
336
376
  # If default exists, add with a unique key
337
- existing_json["examples"]["CustomExample"] = {"value": response_json["example"]}
377
+ unique_key = self._unique_key(existing_json["examples"], "CustomExample")
378
+ existing_json["examples"][unique_key] = {"value": response_json["example"]}
338
379
 
339
380
  def _add_error_dto(self, error_dto: ErrorDTO) -> None:
340
381
  """Add error from ErrorDTO (via Protocol).
@@ -373,6 +414,11 @@ class Errors(Mapping):
373
414
  # Merge examples for the same status code
374
415
  # Safely access content/application/json with defaults
375
416
  existing = self._responses[status_code]
417
+
418
+ # If description is standard, replace it with DTO message (priority: DTO > flags)
419
+ if existing.get("description") == self.STANDARD_DESCRIPTIONS.get(status_code):
420
+ existing["description"] = error_dto.message
421
+
376
422
  existing_content = existing.setdefault("content", {})
377
423
  content_json = existing_content.setdefault("application/json", {})
378
424
  existing_examples = content_json.get("examples", {})
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-errors-plus
3
- Version: 0.4.0
3
+ Version: 0.5.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
@@ -375,11 +375,11 @@ Errors(
375
375
  *errors: Union[Dict[int, Dict[str, Any]], ErrorDTO],
376
376
  unauthorized: bool = False,
377
377
  forbidden: bool = False,
378
- validation_error: bool = True, # Defaults to True (FastAPI validates all parameters)
378
+ validation_error: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
379
379
  internal_server_error: bool = False,
380
380
  unauthorized_401: bool = False,
381
381
  forbidden_403: bool = False,
382
- validation_error_422: bool = True, # Defaults to True (FastAPI validates all parameters)
382
+ validation_error_422: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
383
383
  internal_server_error_500: bool = False,
384
384
  )
385
385
  ```
@@ -388,18 +388,25 @@ Errors(
388
388
  - `*errors`: Arbitrary errors as dict or ErrorDTO objects
389
389
  - `unauthorized_401`: Add 401 Unauthorized error (recommended, explicit). Defaults to `False`.
390
390
  - `forbidden_403`: Add 403 Forbidden error (recommended, explicit). Defaults to `False`.
391
- - `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit). Defaults to `True` (FastAPI validates all parameters).
391
+ - `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit).
392
+ - `None` (default): Add 422 (True by default, FastAPI validates all parameters)
393
+ - `False`: Explicitly disable 422
394
+ - `True`: Explicitly enable 422
392
395
  - `internal_server_error_500`: Add 500 Internal Server Error (recommended, explicit). Defaults to `False`.
393
396
  - `unauthorized`: Add 401 Unauthorized error (legacy, for backward compatibility). Defaults to `False`.
394
397
  - `forbidden`: Add 403 Forbidden error (legacy, for backward compatibility). Defaults to `False`.
395
- - `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility). Defaults to `True` (FastAPI validates all parameters).
398
+ - `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility).
399
+ - `None` (default): Add 422 (True by default, FastAPI validates all parameters)
400
+ - `False`: Explicitly disable 422
401
+ - `True`: Explicitly enable 422
396
402
  - `internal_server_error`: Add 500 Internal Server Error (legacy, for backward compatibility). Defaults to `False`.
397
403
 
398
404
  **Why `validation_error=True` by default?**
399
405
  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`.
400
406
 
401
407
  **Returns:**
402
- - Callable object that returns `Dict[int, Dict[str, Any]]` in FastAPI responses format
408
+ - Mapping object (dict-like) that implements `Dict[int, Dict[str, Any]]` for FastAPI responses
409
+ - Can be used directly in `responses` parameter without calling: `responses=Errors(...)`
403
410
 
404
411
  #### Usage
405
412
 
@@ -5,6 +5,7 @@ fastapi_errors_plus/__init__.py
5
5
  fastapi_errors_plus/base.py
6
6
  fastapi_errors_plus/errors.py
7
7
  fastapi_errors_plus/protocol.py
8
+ fastapi_errors_plus/py.typed
8
9
  fastapi_errors_plus.egg-info/PKG-INFO
9
10
  fastapi_errors_plus.egg-info/SOURCES.txt
10
11
  fastapi_errors_plus.egg-info/dependency_links.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-errors-plus"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "Universal library for documenting errors in FastAPI endpoints"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -302,3 +302,4 @@ class TestBaseErrorDTOCompatibility:
302
302
  assert status.HTTP_404_NOT_FOUND in responses
303
303
  assert status.HTTP_500_INTERNAL_SERVER_ERROR in responses
304
304
 
305
+
@@ -446,6 +446,82 @@ class TestErrorsEdgeCases:
446
446
  assert len(errors) == 1
447
447
 
448
448
 
449
+ class TestUniqueKeys:
450
+ """Tests for unique key generation to prevent collisions."""
451
+
452
+ def test_unique_keys_for_standard_errors(self):
453
+ """Test that standard error keys are unique when merging."""
454
+ # First add flag (creates example), then add dict with examples containing same key
455
+ errors = Errors(
456
+ {401: { # Positional first - adds "StandardUnauthorized" to examples
457
+ "content": {
458
+ "application/json": {
459
+ "examples": {
460
+ "StandardUnauthorized": {"value": {"detail": "Custom"}},
461
+ },
462
+ },
463
+ },
464
+ }},
465
+ unauthorized_401=True, # Named after positional - should use unique key
466
+ )
467
+
468
+ # Should have both examples
469
+ examples = errors[401]["content"]["application/json"]["examples"]
470
+ # When dict is added first with "StandardUnauthorized", then flag is added
471
+ # Flag creates "example" which gets converted to "default" (not "StandardUnauthorized_2")
472
+ # This is because flag creates "example" first, then it's converted
473
+ assert "StandardUnauthorized" in examples # From dict
474
+ assert "default" in examples # From flag (converted from example)
475
+
476
+ # Both examples should be present
477
+ assert len(examples) == 2
478
+ assert examples["StandardUnauthorized"]["value"]["detail"] == "Custom"
479
+ assert examples["default"]["value"]["detail"] == "Unauthorized"
480
+
481
+ def test_unique_keys_for_dict_errors(self):
482
+ """Test that dict error keys are unique when merging."""
483
+ errors = Errors(
484
+ {401: {
485
+ "content": {
486
+ "application/json": {
487
+ "example": {"detail": "First"},
488
+ },
489
+ },
490
+ }},
491
+ {401: {
492
+ "content": {
493
+ "application/json": {
494
+ "example": {"detail": "Second"},
495
+ },
496
+ },
497
+ }},
498
+ )
499
+
500
+ examples = errors[401]["content"]["application/json"]["examples"]
501
+ assert "default" in examples
502
+ # Second example should have unique key
503
+ assert "CustomExample" in examples or "CustomExample_2" in examples
504
+ assert len(examples) == 2
505
+
506
+ def test_unique_keys_multiple_collisions(self):
507
+ """Test unique key generation with multiple collisions."""
508
+ errors = Errors()
509
+ examples = {
510
+ "CustomExample": {"value": {"detail": "First"}},
511
+ "CustomExample_2": {"value": {"detail": "Second"}},
512
+ }
513
+
514
+ # Add another CustomExample - should become CustomExample_3
515
+ unique_key = errors._unique_key(examples, "CustomExample")
516
+ assert unique_key == "CustomExample_3"
517
+
518
+ examples[unique_key] = {"value": {"detail": "Third"}}
519
+
520
+ # Add another - should become CustomExample_4
521
+ unique_key = errors._unique_key(examples, "CustomExample")
522
+ assert unique_key == "CustomExample_4"
523
+
524
+
449
525
  class TestErrorsValidation:
450
526
  """Tests for ErrorDTO validation."""
451
527
 
@@ -531,3 +607,50 @@ class TestErrorsValidation:
531
607
  assert "to_example" in error_msg
532
608
  assert "BadObject" in error_msg
533
609
 
610
+
611
+ class TestDTODescriptionPriority:
612
+ """Tests for DTO description priority over standard flags."""
613
+
614
+ def test_dto_overrides_standard_description(self):
615
+ """Test that ErrorDTO description overrides standard flag description."""
616
+ from fastapi_errors_plus import StandardErrorDTO
617
+
618
+ custom_401 = StandardErrorDTO(
619
+ status_code=401,
620
+ message="Custom Unauthorized",
621
+ examples={"Custom": "Custom auth error"},
622
+ )
623
+
624
+ errors = Errors(
625
+ custom_401, # Positional first
626
+ unauthorized_401=True, # Named after - adds "Unauthorized"
627
+ )
628
+
629
+ # DTO should override standard description
630
+ assert errors[401]["description"] == "Custom Unauthorized"
631
+
632
+ def test_dto_does_not_override_custom_description(self):
633
+ """Test that DTO does not override non-standard description."""
634
+ from fastapi_errors_plus import StandardErrorDTO
635
+
636
+ custom_401 = StandardErrorDTO(
637
+ status_code=401,
638
+ message="Another Custom",
639
+ examples={"Custom": "Custom auth error"},
640
+ )
641
+
642
+ errors = Errors(
643
+ {401: { # Positional first - custom description
644
+ "description": "My Custom Description",
645
+ "content": {
646
+ "application/json": {
647
+ "example": {"detail": "Custom"},
648
+ },
649
+ },
650
+ }},
651
+ custom_401, # DTO should not override dict description
652
+ )
653
+
654
+ # Dict description should be preserved (dict > DTO)
655
+ assert errors[401]["description"] == "My Custom Description"
656
+