Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- name: Checkout source code
Expand All @@ -28,7 +28,7 @@ jobs:
python -m pip install tox tox-gh-actions

- name: Run tox
run: tox
run: tox -v

- name: Upload coverage reports to Codecov
uses: codecov/[email protected]
Expand Down
29 changes: 26 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,43 @@

## [1.0.0] - 2024-04-26

Changes:

- First release on PyPI

## [1.0.1] - 2024-04-30

Changes:

- Upgrade dependencies

## [1.0.2] - 2024-09-08

- Add docstrings to handlers
Changes:

- Add docstrings
- Improve Makefile
- Improve README

## [1.0.3] - 2025-03-16

- Update README
- Improve tests
Changes:

- Fix DRF API settings initialization
- Improve tests
- Update README

## [2.0.0] - 2025-07-11

Breaking changes:

- The API error response now **always** includes the keys: `title`, `detail`, and `invalid_param`. The `title` key is always populated, while `detail` and `invalid_param` may be `null` depending on the error source.
- Drop support for python 3.8

Changes:

- Improve code modularity and readability
- Split tests in unittest and integration tests
- Improve test coverage
- Update Makefile
- Update README
18 changes: 7 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,21 @@
install:
@poetry install --with dev -v

# Run poetry shell
shell:
@poetry shell

# Run formatters (black, isort) with poetry
format:
@poetry run isort drf_simple_api_errors test_project
@poetry run black drf_simple_api_errors test_project
@poetry run isort drf_simple_api_errors integration_tests tests
@poetry run black drf_simple_api_errors integration_tests tests

# Check format (black, isort) and linting (flake8)
lint:
@poetry run isort --check drf_simple_api_errors test_project
@poetry run black --check drf_simple_api_errors test_project --exclude migrations
@poetry run flake8 drf_simple_api_errors test_project --max-line-length 88
@poetry run isort --check drf_simple_api_errors integration_tests tests
@poetry run black --check drf_simple_api_errors integration_tests tests --exclude migrations
@poetry run flake8 drf_simple_api_errors integration_tests tests --max-line-length 88

# Run unittests with poetry
test:
@poetry run pytest test_project
@poetry run pytest

# Run code coverage tests coverage with poetry
test/cov:
@poetry run pytest test_project --cov=drf_simple_api_errors --cov-report xml:coverage.xml
@poetry run pytest --cov=drf_simple_api_errors --cov-report xml:coverage.xml
148 changes: 48 additions & 100 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@

A library for [Django Rest Framework](https://www.django-rest-framework.org/) returning **consistent, predictable and easy-to-parse API error messages**.

This library was built with [RFC7807](https://tools.ietf.org/html/rfc7807) guidelines in mind, but with a small twist: it defines a "problem detail" as a list, but it still serves as a way to include errors in a predictable and easy-to-parse format for any API consumer. Error messages are formatted using RFC7807 keywords and DRF exception data.
This library was built with [RFC7807](https://tools.ietf.org/html/rfc7807) guidelines in mind, but with a small twist: it defines a "problem detail" as a list instead of a string, but it still serves as a way to include errors in a human-readable and easy-to-parse format for any API consumer.
Error messages are formatted using RFC7807 keywords and DRF exception data.

Unlike standard DRF, where the error response format varies depending on the error source, this library always returns errors in a consistent and predictable structure.

## What's different?

Compared to other similar and popular libraries, this library:

- Is based on RFC7807 guidelines
- Aims to provide not only a standardized format for error details, but also human-readable error messages (perfect for both internal and public APIs)
- Transforms both `django.core.exceptions.ValidationError` and `rest_framework.errors.ValidationError` to API errors, so you don't have to handle error raised by services/domain logic, `clean()`, or other functions/methods
- Transforms both `django.core.exceptions.ValidationError` and `rest_framework.errors.ValidationError` to API errors, so you don't have to handle error raised by services/domain logic, `clean()`, etc.

## Table of Contents

Expand Down Expand Up @@ -57,13 +60,13 @@ REST_FRAMEWORK = {

### Error structure overview

API error messages typically include the following keys:
API error messages will include the following keys:

- `"title"` (`str`): A brief summary that describes the problem type
- `"detail"` (`list[str] | None`): A list of specific explanations related to the problem
- `"invalid_params"` (`list[dict] | None`): A list of dict containing details about parameters that were invalid or malformed in the request. Each dict within this list provides:
- `"name"` (`str`): The name of the parameter that was found to be invalid
- `"reasons"` (`list[str]`): A list of strings describing the specific reasons why the parameter was considered invalid or malformed
- `"title"` (`str`): A brief summary that describes the problem type.
- `"detail"` (`list[str] | None`): A list of specific explanations related to the problem, if any.
- `"invalid_params"` (`list[dict] | None`): A list of dict containing details about parameters that were invalid or malformed in the request, if any. Each dict within this list provides:
- `"name"` (`str`): The name of the parameter that was found to be invalid.
- `"reasons"` (`list[str]`): A list of strings describing the specific reasons why the parameter was considered invalid or malformed.

```json
{
Expand Down Expand Up @@ -91,17 +94,18 @@ API error messages typically include the following keys:

```json
{
"title": "Error message.",
"invalid_params": [
{
"name": "field_name",
"reason": [
"error",
...
]
},
...
]
"title": "Error message.",
"details": null,
"invalid_params": [
{
"name": "field_name",
"reason": [
"error"
// ...
]
}
// ...
]
}
```

Expand All @@ -111,23 +115,26 @@ API error messages typically include the following keys:
{
"title": "Error message.",
"detail": [
"error",
...
]
"error"
// ...
],
"invalid_params": null
}
```

#### Other bad requests with no detail

```json
{
"title": "Error message."
"title": "Error message.",
"detail": null,
"invalid_params": null
}
```

## Settings

Default available settings:
Default settings:

```python
DRF_SIMPLE_API_ERRORS = {
Expand All @@ -146,15 +153,16 @@ If `CAMELIZE` is set to `True`:
```json
{
"title": "Error message.",
"details": null,
"invalidParams": [
{
"name": "fieldName",
"reason": [
"error",
...
"error"
// ...
]
}
...
// ...
]
}
```
Expand All @@ -163,89 +171,23 @@ If `CAMELIZE` is set to `True`:

Support for exceptions that differ from the standard structure of the Django Rest Framework.

For instance, you may want to specify you own exception:

```python
class AuthenticationFailed(exceptions.AuthenticationFailed):
def __init__(self, detail=None, code=None):
"""
Builds a detail dictionary for the error to give more information
to API users.
"""
detail_dict = {"detail": self.default_detail, "code": self.default_code}

if isinstance(detail, dict):
detail_dict.update(detail)
elif detail is not None:
detail_dict["detail"] = detail

if code is not None:
detail_dict["code"] = code

super().__init__(detail_dict)
```

Use exception in code:

```python
def my_func():
raise AuthenticationFailed(
{
"detail": _("Error message."),
"messages": [
{
"metadata": "metadata_data",
"type": "type_name",
"message": "error message",
}
],
}
)
```

This will result in:
For example, if you need to customize how a specific exception is handled or want to format an existing exception differently, you can create your own handler.

```python
AuthenticationFailed(
{
"detail": "Error message.",
"messages": [
{
"metadata": "metadata_data",
"type": "type_name",
"message": "error message",
}
],
}
)
```

You can handle this by creating a `handlers.py` file and specifying an handler for your use case:

```python
def handle_exc_custom_authentication_failed(exc):
from path.to.my.exceptions import AuthenticationFailed

if isinstance(exc, AuthenticationFailed):
try:
exc.detail = exc.detail["messages"][0]["message"]
except (KeyError, IndexError):
exc.detail = exc.detail["detail"]

return exc
```
To customize error handling for your project, simply create a new file (for example, `extra_handlers.py`) and define your own handler functions. This approach lets you tailor error responses to fit your specific needs.

Then add it to the `EXTRA_HANDLERS` list in this package settings:

```python
DRF_SIMPLE_API_ERRORS = {
"EXTRA_HANDLERS": [
"path.to.my.handlers.handle_exc_custom_authentication_failed",
"path.to.my.extra_handlers.custom_handler",
# ...
]
}
```

For reference, this library uses the same pattern for its own extra handlers [here](drf_simple_api_errors/extra_handlers.py).

- #### FIELDS_SEPARATOR

Support for nested dicts containing multiple fields to be flattened.
Expand Down Expand Up @@ -274,13 +216,19 @@ All the necessary commands are included in the `Makefile`.

We are using `tox` and `poetry` to run tests in every supported Python version.

Run test with the commands below:
Run test during development with the commands below:

```
make install
make install # only if necessary
make test
```

Finally, run `tox` to ensure the changes work for every supported python version:

```
tox -v
```

## Support

Please [open an issue](https://github.com/gripep/drf-simple-api-errors/issues/new).
Expand Down
2 changes: 1 addition & 1 deletion drf_simple_api_errors/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .exception_handler import exception_handler

__all__ = ("exception_handler",)
__version__ = "1.0.3"
__version__ = "2.0.0"
Loading