Skip to content

Commit

Permalink
Add a method for customizing validation errors
Browse files Browse the repository at this point in the history
  • Loading branch information
jceipek committed Jan 2, 2025
1 parent 39f40b3 commit 824df53
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 21 deletions.
20 changes: 19 additions & 1 deletion ninja/errors.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import logging
import traceback
from functools import partial
from typing import TYPE_CHECKING, List, Optional
from typing import TYPE_CHECKING, Generic, List, Optional, TypeVar

import pydantic
from django.conf import settings
from django.http import Http404, HttpRequest, HttpResponse

from ninja.types import DictStrAny

if TYPE_CHECKING:
from ninja import NinjaAPI # pragma: no cover
from ninja.params.models import ParamModel # pragma: no cover

__all__ = [
"ConfigError",
Expand All @@ -28,6 +30,22 @@ class ConfigError(Exception):
pass


TModel = TypeVar("TModel", bound="ParamModel")


class ValidationErrorContext(Generic[TModel]):
"""
The full context of a `pydantic.ValidationError`, including all information
needed to produce a `ninja.errors.ValidationError`.
"""

def __init__(
self, pydantic_validation_error: pydantic.ValidationError, model: TModel
):
self.pydantic_validation_error = pydantic_validation_error
self.model = model


class ValidationError(Exception):
"""
This exception raised when operation params do not validate
Expand Down
29 changes: 28 additions & 1 deletion ninja/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
from django.utils.module_loading import import_string

from ninja.constants import NOT_SET, NOT_SET_TYPE
from ninja.errors import ConfigError, set_default_exc_handlers
from ninja.errors import (
ConfigError,
ValidationError,
ValidationErrorContext,
set_default_exc_handlers,
)
from ninja.openapi import get_schema
from ninja.openapi.docs import DocsBase, Swagger
from ninja.openapi.schema import OpenAPISchema
Expand Down Expand Up @@ -514,6 +519,28 @@ def on_exception(self, request: HttpRequest, exc: Exc[_E]) -> HttpResponse:
raise exc
return handler(request, exc)

def validation_error_from_error_contexts(
self, error_contexts: List[ValidationErrorContext]
) -> ValidationError:
errors: List[Dict[str, Any]] = []
for context in error_contexts:
model = context.model
e = context.pydantic_validation_error
for i in e.errors(include_url=False):
i["loc"] = (
model.__ninja_param_source__,
) + model.__ninja_flatten_map_reverse__.get(i["loc"], i["loc"])
# removing pydantic hints
del i["input"] # type: ignore
if (
"ctx" in i
and "error" in i["ctx"]
and isinstance(i["ctx"]["error"], Exception)
):
i["ctx"]["error"] = str(i["ctx"]["error"])
errors.append(dict(i))
return ValidationError(errors)

def _lookup_exception_handler(self, exc: Exc[_E]) -> Optional[ExcHandler[_E]]:
for cls in type(exc).__mro__:
if cls in self._exception_handlers:
Expand Down
35 changes: 16 additions & 19 deletions ninja/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
from django.http.response import HttpResponseBase

from ninja.constants import NOT_SET, NOT_SET_TYPE
from ninja.errors import AuthenticationError, ConfigError, Throttled, ValidationError
from ninja.errors import (
AuthenticationError,
ConfigError,
Throttled,
ValidationErrorContext,
)
from ninja.params.models import TModels
from ninja.schema import Schema, pydantic_version
from ninja.signature import ViewSignature, is_async
Expand Down Expand Up @@ -282,29 +287,21 @@ def _result_to_response(
def _get_values(
self, request: HttpRequest, path_params: Any, temporal_response: HttpResponse
) -> DictStrAny:
values, errors = {}, []
values = {}
error_contexts: List[ValidationErrorContext] = []
for model in self.models:
try:
data = model.resolve(request, self.api, path_params)
values.update(data)
except pydantic.ValidationError as e:
items = []
for i in e.errors(include_url=False):
i["loc"] = (
model.__ninja_param_source__,
) + model.__ninja_flatten_map_reverse__.get(i["loc"], i["loc"])
# removing pydantic hints
del i["input"] # type: ignore
if (
"ctx" in i
and "error" in i["ctx"]
and isinstance(i["ctx"]["error"], Exception)
):
i["ctx"]["error"] = str(i["ctx"]["error"])
items.append(dict(i))
errors.extend(items)
if errors:
raise ValidationError(errors)
error_contexts.append(
ValidationErrorContext(pydantic_validation_error=e, model=model)
)
if error_contexts:
validation_error = self.api.validation_error_from_error_contexts(
error_contexts
)
raise validation_error
if self.signature.response_arg:
values[self.signature.response_arg] = temporal_response
return values
Expand Down

0 comments on commit 824df53

Please sign in to comment.