Universal library for documenting errors in FastAPI endpoints
Project description
fastapi-errors-plus
Universal library for documenting errors in FastAPI endpoints.
Philosophy
fastapi-errors-plus is designed to be universal and work with any FastAPI project without requiring project-specific infrastructure. The library uses standard Python types (dict, Protocol) and allows projects to adapt their existing error infrastructure to work with the library through structural typing.
Key principles:
- Universality — works with any FastAPI project
- Transparency — all documented errors are visible directly in the endpoint
- Self-sufficiency — no need to search other files to understand documented errors
- Compatibility — works with existing project infrastructure through Protocol
Installation
pip install fastapi-errors-plus
Or install from source:
git clone git@github.com:seligoroff/fastapi-errors-plus.git
cd fastapi-errors-plus
pip install -e .
Quick Start
Basic Usage
from fastapi import APIRouter
from fastapi_errors_plus import Errors
router = APIRouter()
@router.delete(
"/{id}",
responses=Errors(
{404: { # 404 via dict
"description": "Not found",
"content": {
"application/json": {
"example": {"detail": "Item not found"},
},
},
}},
unauthorized_401=True, # 401 Unauthorized (explicit)
forbidden_403=True, # 403 Forbidden (explicit)
# validation_error_422=True - not needed, defaults to True
),
)
def delete_item(id: int):
"""Delete an item."""
# ... your code ...
pass
Features
1. Standard HTTP Status Flags
Use boolean flags for common HTTP status codes:
Recommended (explicit status codes):
unauthorized_401=True→ 401 Unauthorizedforbidden_403=True→ 403 Forbiddenvalidation_error_422=True→ 422 Unprocessable Entity (defaults toTrue)internal_server_error_500=True→ 500 Internal Server Error
Legacy (for backward compatibility):
unauthorized=True→ 401 Unauthorizedforbidden=True→ 403 Forbiddenvalidation_error=True→ 422 Unprocessable Entity (defaults toTrue)internal_server_error=True→ 500 Internal Server Error
Note: validation_error and validation_error_422 default to True because FastAPI automatically validates all parameters (Path, Query, Body), making 422 relevant in 95%+ of endpoints. Set to False only for endpoints without parameters.
@router.get(
"/protected",
responses=Errors(
unauthorized_401=True, # Explicit: 401 is visible
forbidden_403=True, # Explicit: 403 is visible
),
)
def get_protected():
"""Protected endpoint."""
pass
Why explicit flags? The new flags with status codes (_401, _403, etc.) make it immediately clear which HTTP status code corresponds to each flag, improving code readability without needing to remember the mapping.
2. Dict-based Errors
Use standard FastAPI responses dict format for custom errors:
@router.post(
"/items",
responses=Errors(
{
409: {
"description": "Conflict",
"content": {
"application/json": {
"example": {"detail": "Item already exists"},
},
},
},
}
),
)
def create_item():
"""Create an item."""
pass
3. ErrorDTO Protocol
Use objects implementing the ErrorDTO protocol for project compatibility:
from fastapi_errors_plus import Errors, ErrorDTO
class MyErrorDTO:
status_code = 404
message = "Not found"
def to_example(self):
return {
"Not found": {
"value": {"detail": "Not found"},
},
}
@router.get(
"/resource/{id}",
responses=Errors(
MyErrorDTO(),
),
)
def get_resource(id: int):
"""Get a resource."""
pass
4. BaseErrorDTO and StandardErrorDTO (Recommended)
For convenience, the library provides ready-to-use implementations:
BaseErrorDTO
Simple implementation for errors with a single example:
from fastapi_errors_plus import Errors, BaseErrorDTO
notification_error = BaseErrorDTO(
status_code=404,
message="Notification not found",
)
@router.delete(
"/{id}",
responses=Errors(notification_error),
)
def delete_item(id: int):
"""Delete an item."""
pass
OpenAPI extras (schema) next to examples
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):
from fastapi import APIRouter, status
from fastapi_errors_plus import BaseErrorDTO, Errors
ADR_ERROR_BODY_SCHEMA = {
"type": "object",
"required": ["code", "detail"],
"properties": {
"code": {"type": "string"},
"detail": {"type": "string"},
"context": {"type": "object"},
},
}
business_conflict = BaseErrorDTO(
status_code=status.HTTP_409_CONFLICT,
message="BusinessRuleViolation",
openapi_json_extras={"schema": ADR_ERROR_BODY_SCHEMA},
)
router = APIRouter()
@router.post("/items", responses=Errors(business_conflict, validation_error=False))
def create_item():
...
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).
StandardErrorDTO
Extended implementation for errors with multiple examples (useful for standard HTTP errors):
from fastapi_errors_plus import Errors, StandardErrorDTO
unauthorized_error = StandardErrorDTO(
status_code=401,
message="Unauthorized",
examples={
"InvalidToken": "Ошибка декодирования токена.",
"SessionNotFound": "Сессия пользователя не была найдена.",
},
)
forbidden_error = StandardErrorDTO(
status_code=403,
message="Forbidden",
examples={
"AccountNotSelected": "Аккаунт не выбран.",
"RoleHasNoAccess": "Роль не имеет доступа.",
},
)
@router.delete(
"/{id}",
responses=Errors(
unauthorized_error,
forbidden_error,
# validation_error=True - not needed, defaults to True
),
)
def delete_item(id: int):
"""Delete an item."""
pass
Benefits:
- No need to write ErrorDTO classes from scratch
- Correct implementation out of the box
- Reusable across all endpoints
- Supports inheritance for custom logic
5. Mixed Usage
Combine flags, dict, and ErrorDTO:
@router.post(
"/items/{id}",
responses=Errors(
{409: {
"description": "Conflict",
"content": {
"application/json": {
"example": {"detail": "Already exists"},
},
},
}},
MyErrorDTO(), # ErrorDTO
unauthorized=True, # Flag
forbidden=True, # Flag
# validation_error=True - not needed, defaults to True
),
)
def create_item_mixed(id: int):
"""Create an item with mixed error types."""
pass
6. Merging Examples
Multiple errors with the same status code are automatically merged:
@router.put(
"/items/{id}",
responses=Errors(
Error1(), # 404
Error2(), # 404
),
)
def update_item(id: int):
"""Update an item."""
pass
The OpenAPI spec will contain both examples under the 404 status code.
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).
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).
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:
Errors(
conflict_error_doc, # implements ErrorDTO, e.g. .for_openapi() for ADR-shaped examples
{
status.HTTP_409_CONFLICT: {
"description": "Business rule violation",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {"type": "string"},
"detail": {"type": "string"},
"context": {"type": "object"},
},
},
},
},
},
},
)
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.
ErrorDTO Protocol
The ErrorDTO protocol defines the interface for error objects compatible with the library:
from typing import Protocol, Dict, Any
class ErrorDTO(Protocol):
status_code: int
message: str
def to_example(self) -> Dict[str, Any]:
"""Generate example for OpenAPI.
Returns:
Dict in format: {"key": {"value": {"detail": "message"}}}
"""
...
Any class implementing this protocol (through structural typing) can be used with Errors().
Best Practice: For maximum clarity, consider making your domain exceptions implement the ErrorDTO protocol directly. See Best Practice: Connecting Exceptions and ErrorDTO for details.
When to Use Protocol vs BaseErrorDTO
Use Protocol (structural typing) when:
- Your project already has error DTOs that implement the protocol
- You need maximum flexibility and custom implementations
- You want to keep your existing error infrastructure
Use BaseErrorDTO/StandardErrorDTO when:
- Starting a new project or adding error documentation
- You want a ready-to-use implementation without boilerplate
- You need multiple examples for standard HTTP errors (401, 403, etc.)
Both approaches work together — you can mix them in the same Errors() call!
Using Pydantic with ErrorDTO
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.
Since the library uses structural typing (Protocol), any class that implements the required attributes (status_code, message, to_example()) will work, including Pydantic models.
Simple Pydantic Model as ErrorDTO
from pydantic import BaseModel, Field
from fastapi_errors_plus import Errors
from typing import Dict, Any
class PydanticErrorDTO(BaseModel):
"""Pydantic model implementing ErrorDTO Protocol."""
status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
message: str = Field(..., min_length=1, description="Error message")
def to_example(self) -> Dict[str, Any]:
"""Generate example for OpenAPI."""
return {
self.message: {
"value": {"detail": self.message},
},
}
# Usage
notification_error = PydanticErrorDTO(
status_code=404,
message="Notification not found",
)
@router.delete(
"/{id}",
responses=Errors(notification_error),
)
def delete_item(id: int):
pass
Benefits:
- Runtime validation through Pydantic
- Type safety
- Automatic field documentation
- Works with ErrorDTO Protocol through structural typing
Complex ErrorDTO with Pydantic
For errors with additional fields:
from pydantic import BaseModel, Field
from fastapi_errors_plus import Errors
from typing import Dict, Any, Optional
class DetailedErrorDTO(BaseModel):
"""Pydantic model for errors with additional fields."""
status_code: int = Field(..., ge=400, le=599)
message: str = Field(..., min_length=1)
error_code: Optional[str] = Field(None, description="Internal error code")
timestamp: Optional[str] = Field(None, description="Error timestamp")
def to_example(self) -> Dict[str, Any]:
"""Generate example for OpenAPI."""
example = {"detail": self.message}
if self.error_code:
example["error_code"] = self.error_code
if self.timestamp:
example["timestamp"] = self.timestamp
return {
self.message: {
"value": example,
},
}
# Usage
validation_error = DetailedErrorDTO(
status_code=422,
message="Validation failed",
error_code="VALIDATION_ERROR",
timestamp="2025-01-15T10:30:00Z",
)
When to use Pydantic with ErrorDTO:
- Your project already uses Pydantic extensively
- You need runtime validation for error objects
- You want automatic field documentation
- You have complex error structures with multiple fields
When not to use Pydantic:
- Your project doesn't use Pydantic (use
BaseErrorDTOorStandardErrorDTOinstead) - You need simple error objects (dataclasses are sufficient)
- You want to keep dependencies minimal
Best Practice: Connecting Exceptions and ErrorDTO
Problem
It's not always clear which exception corresponds to which ErrorDTO:
# Not clear which exception this documents
responses=Errors(notification_not_found_error)
Solution: Domain Exception as ErrorDTO
Recommended approach — make your exceptions implement ErrorDTO protocol:
# domain/exceptions.py
from typing import Dict, Any
class DomainException(Exception):
"""Base exception implementing ErrorDTO protocol."""
status_code: int
message: str
def to_example(self) -> Dict[str, Any]:
return {self.message: {"value": {"detail": self.message}}}
@classmethod
def for_openapi(cls):
"""Returns instance for OpenAPI documentation."""
return cls()
class NotificationNotFoundError(DomainException):
status_code = 404
message = "Notification not found"
def __init__(self, notification_id: str = ""):
self.notification_id = notification_id
super().__init__(self.message)
@classmethod
def for_openapi(cls):
return cls(notification_id="example_id")
# In endpoint
@router.delete(
"/{notificationId}",
responses=Errors(
NotificationNotFoundError.for_openapi(), # Clear connection!
),
)
async def delete_notification(notification_id: str):
if not notification:
raise NotificationNotFoundError(notification_id) # Same exception!
Benefits:
- Exception and ErrorDTO are one class
- Clear connection visible in endpoint
- No duplication
- Type-safe
- Works with any project architecture
See examples/domain_exceptions.py for complete example.
Compatibility with Existing Projects
If your project already has error DTOs (like ApiErrorDTO), they can work with fastapi-errors-plus if they implement the ErrorDTO protocol:
# Your existing ApiErrorDTO
@dataclass
class ApiErrorDTO:
status_code: int
message: str
def to_example(self) -> dict:
return {
self.message: {
"value": {"detail": self.message},
},
}
# Works directly with fastapi-errors-plus!
@router.delete(
"/{id}",
responses=Errors(
ApiErrorDTO(status_code=404, message="Not found"),
),
)
def delete_item(id: int):
pass
API Reference
Errors
Main class for documenting errors in FastAPI endpoints.
Constructor
Errors(
*errors: Union[Dict[int, Dict[str, Any]], ErrorDTO],
unauthorized: bool = False,
forbidden: bool = False,
validation_error: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
internal_server_error: bool = False,
unauthorized_401: bool = False,
forbidden_403: bool = False,
validation_error_422: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
internal_server_error_500: bool = False,
)
Parameters:
*errors: Arbitrary errors as dict or ErrorDTO objectsunauthorized_401: Add 401 Unauthorized error (recommended, explicit). Defaults toFalse.forbidden_403: Add 403 Forbidden error (recommended, explicit). Defaults toFalse.validation_error_422: Add 422 Unprocessable Entity error (recommended, explicit).None(default): Add 422 (True by default, FastAPI validates all parameters)False: Explicitly disable 422True: Explicitly enable 422
internal_server_error_500: Add 500 Internal Server Error (recommended, explicit). Defaults toFalse.unauthorized: Add 401 Unauthorized error (legacy, for backward compatibility). Defaults toFalse.forbidden: Add 403 Forbidden error (legacy, for backward compatibility). Defaults toFalse.validation_error: Add 422 Unprocessable Entity error (legacy, for backward compatibility).None(default): Add 422 (True by default, FastAPI validates all parameters)False: Explicitly disable 422True: Explicitly enable 422
internal_server_error: Add 500 Internal Server Error (legacy, for backward compatibility). Defaults toFalse.
Why validation_error=True by default?
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.
Returns:
- A dict-like
Mapping[int, …]suitable for FastAPI’sresponses/ OpenAPI - Pass the instance as-is — do not call it like a function:
responses=Errors(...)
Usage
# Instances are Mapping keyed by HTTP status codes
error_responses = Errors(unauthorized_401=True, forbidden_403=True)
documented = error_responses[401] # response block picked up by OpenAPI
ErrorDTO
Protocol for error objects compatible with the library.
Required attributes:
status_code: int— HTTP status codemessage: str— Error message description
Required methods:
to_example() -> Dict[str, Any]— Generate example for OpenAPI
During Errors(...) initialization, non-dict objects in *errors missing status_code, message, or a callable to_example raise TypeError naming what was missing.
BaseErrorDTO
Base implementation of ErrorDTO Protocol for convenience.
Constructor:
BaseErrorDTO(
status_code: int,
message: str,
openapi_json_extras: Optional[Dict[str, Any]] = None,
)
Example:
error = BaseErrorDTO(status_code=404, message="Not found")
StandardErrorDTO
Extended implementation for errors with multiple examples.
Constructor:
StandardErrorDTO(
status_code: int,
message: str,
openapi_json_extras: Optional[Dict[str, Any]] = None,
examples: Optional[Dict[str, str]] = None,
)
Example:
error = StandardErrorDTO(
status_code=401,
message="Unauthorized",
examples={
"InvalidToken": "Invalid token",
"SessionNotFound": "Session not found",
},
)
Examples
Example 1: Standard FastAPI Project
from fastapi import APIRouter
from fastapi_errors_plus import Errors
router = APIRouter()
@router.delete(
"/{id}",
responses=Errors(
{404: {
"description": "Not found",
"content": {
"application/json": {
"example": {"detail": "Item not found"},
},
},
}},
unauthorized=True,
forbidden=True,
),
)
def delete_item(id: int):
"""Delete an item."""
pass
Example 2: Project with ErrorDTO
from fastapi import APIRouter
from fastapi_errors_plus import Errors
from api.exceptions.dto import notification_not_found_error # ErrorDTO-compatible instance
router = APIRouter()
@router.delete(
"/{notificationId}",
responses=Errors(
unauthorized=True,
forbidden=True,
notification_not_found_error,
),
)
async def delete_notification(notification_id: int):
"""Delete a notification."""
pass
Example 3: Multiple Examples for Same Status
@router.delete(
"/{id}",
responses=Errors(
unauthorized=True, # Basic 401
{401: { # Override with multiple examples
"description": "Unauthorized",
"content": {
"application/json": {
"examples": {
"InvalidToken": {"value": {"detail": "Invalid token"}},
"SessionNotFound": {"value": {"detail": "Session not found"}},
},
},
},
}},
),
)
def delete_item(id: int):
"""Delete an item."""
pass
Example 4: Clean Architecture Integration
This example shows how to use fastapi-errors-plus in a FastAPI project with Clean Architecture:
Domain Layer (domain/errors.py):
from typing import Dict, Any
class DomainException(Exception):
"""Domain exception usable as runtime error and shaped like ErrorDTO for OpenAPI."""
status_code: int
message: str
def __init__(self) -> None:
super().__init__(self.message)
def to_example(self) -> Dict[str, Any]:
return {
self.message: {
"value": {"detail": self.message},
},
}
class ItemNotFoundError(DomainException):
status_code = 404
message = "Item not found"
class ItemAlreadyExistsError(DomainException):
status_code = 409
message = "Item already exists"
Application Layer (application/use_cases.py):
from domain.errors import ItemNotFoundError, ItemAlreadyExistsError
class CreateItemUseCase:
"""Use case for creating an item."""
def execute(self, item_data: dict):
# Business logic here
if self._item_exists(item_data["id"]):
raise ItemAlreadyExistsError()
# ... create item ...
return item
class GetItemUseCase:
"""Use case for getting an item."""
def execute(self, item_id: int):
item = self._repository.get(item_id)
if not item:
raise ItemNotFoundError()
return item
Infrastructure/Presentation Layer (api/routes/items.py):
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi_errors_plus import Errors
from domain.errors import ItemNotFoundError, ItemAlreadyExistsError
from application.use_cases import CreateItemUseCase, GetItemUseCase
router = APIRouter()
@router.post(
"/items",
status_code=status.HTTP_201_CREATED,
responses=Errors(
unauthorized=True, # From authentication dependency
forbidden=True, # From authorization dependency
# validation_error=True - not needed, defaults to True (FastAPI validates all parameters)
ItemAlreadyExistsError(), # Domain error
),
)
async def create_item(
item_data: dict,
use_case: CreateItemUseCase = Depends(),
):
"""Create a new item."""
try:
item = use_case.execute(item_data)
return item
except ItemAlreadyExistsError as e:
raise HTTPException(
status_code=e.status_code,
detail=e.message,
)
@router.get(
"/items/{item_id}",
responses=Errors(
unauthorized=True,
forbidden=True,
ItemNotFoundError(), # Domain error
),
)
async def get_item(
item_id: int,
use_case: GetItemUseCase = Depends(),
):
"""Get an item by ID."""
try:
item = use_case.execute(item_id)
return item
except ItemNotFoundError as e:
raise HTTPException(
status_code=e.status_code,
detail=e.message,
)
Benefits of this approach:
- Domain errors are reusable across layers
- Errors are documented directly in the endpoint
- Clean separation of concerns
- Domain layer doesn't depend on FastAPI
- Easy to test domain errors independently
Limitations
The library improves transparency of documented errors. It does not solve the problem of finding all real errors in an endpoint, which requires analyzing the entire codebase (transaction scripts, Depends dependencies, database operations, etc.).
What the library does:
- Improves transparency of documented errors
- Simplifies syntax for documenting errors
- Makes errors visible directly in the endpoint
What the library does not do:
- Find all real errors in an endpoint automatically
- Analyze code to discover errors
- Guarantee completeness of error lists
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT
Changelog
See CHANGELOG.md for detailed changelog.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastapi_errors_plus-0.7.0.tar.gz.
File metadata
- Download URL: fastapi_errors_plus-0.7.0.tar.gz
- Upload date:
- Size: 32.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da32a36c9e48d2275098eb2ef73fe66395dce86b815ebd2ee2603ec465af7114
|
|
| MD5 |
44bf8edc4f0b2f107eff0c7429b9c3ae
|
|
| BLAKE2b-256 |
8bc2b49a936e0c8e5dabbbdd4587a2b3377c45c86d3828873c501ea49f3d6117
|
File details
Details for the file fastapi_errors_plus-0.7.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_errors_plus-0.7.0-py3-none-any.whl
- Upload date:
- Size: 17.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
00e8b9dad45ce696e922ff3051c9689528abfe348c486cd7f5b3b7a42e9f792f
|
|
| MD5 |
2e072925078b0d552857b84bb8da11cb
|
|
| BLAKE2b-256 |
81515497c63419b937c8aa9d2e1a51457b453d63f6cc0eddb8449ae99129a93e
|