Skip to content

Commit

Permalink
Add hook for custom validation error formatting
Browse files Browse the repository at this point in the history
We have some older APIs that don't follow the standard error formatting,
so this allows us to still use the api decorator for these endpoints
without breaking older clients.
  • Loading branch information
ljodal committed Apr 19, 2024
1 parent 884fefc commit dd69745
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 3 deletions.
8 changes: 5 additions & 3 deletions django_api_decorator/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
T = typing.TypeVar("T")

Annotation = Any
ExceptionHandler = Callable[[ValidationError | pydantic.ValidationError], HttpResponse]


def api(
Expand All @@ -32,6 +33,7 @@ def api(
atomic: bool | None = None,
auth_check: Callable[[HttpRequest], bool] | None = None,
serialize_by_alias: bool = False,
validation_error_handler: ExceptionHandler | None = None,
) -> Callable[[Callable[P, T]], Callable[P, HttpResponse]]:
"""
Defines an API view. This handles validation of query parameters, parsing of
Expand Down Expand Up @@ -75,6 +77,7 @@ def api(
if atomic is not None
else getattr(settings, "API_DECORATOR_DEFAULT_ATOMIC", True)
)
validation_error_handler = validation_error_handler or handle_validation_error

def default_auth_check(request: HttpRequest) -> bool:
return hasattr(request, "user") and request.user.is_authenticated
Expand Down Expand Up @@ -138,7 +141,7 @@ def inner(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
pydantic.ValidationError,
) as e:
# Normalize and return a unified error message payload
return handle_validation_error(exception=e)
return validation_error_handler(e)

try:
if atomic:
Expand All @@ -155,7 +158,7 @@ def inner(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
except ValidationError as e:
# Normalize and return a unified error message payload, but only
# for Django's ValidationError error, not DRF or Pydantic
return handle_validation_error(exception=e)
return validation_error_handler(e)

except PublicAPIError as exc:
# Raised custom errors to frontend
Expand Down Expand Up @@ -290,7 +293,6 @@ class PydanticErrorDict(TypedDict):


def handle_validation_error(
*,
exception: (ValidationError | pydantic.ValidationError),
) -> HttpResponse:
errors: list[str]
Expand Down
33 changes: 33 additions & 0 deletions tests/test_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.test.utils import override_settings
from django.urls import path
from pydantic import BaseModel
from pytest_mock import MockerFixture

from django_api_decorator.decorators import api

Expand Down Expand Up @@ -344,3 +345,35 @@ def view(request: HttpRequest, body: list[Body]) -> JsonResponse:
)
assert response.status_code == 400
assert response.json()["field_errors"].keys() == {"0.num", "1.num", "1.d"}


@override_settings(ROOT_URLCONF=__name__)
def test_custom_exception_handler(client: Client, mocker: MockerFixture) -> None:
"""
Test that a custom validation error handler is called
"""

def handle_exception(e: Exception) -> JsonResponse:
return JsonResponse({"error": "Something is wrong here"}, status=400)

class Body(BaseModel):
num: int
d: datetime.date

@api(
method="POST",
login_required=False,
validation_error_handler=handle_exception,
)
def view(request: HttpRequest, body: list[Body]) -> JsonResponse:
return JsonResponse({})

urls = [
path("", view),
]

mocker.patch(f"{__name__}.urlpatterns", urls)

response = client.post("/", data={}, content_type="application/json")
assert response.status_code == 400
assert response.json() == {"error": "Something is wrong here"}

0 comments on commit dd69745

Please sign in to comment.