-
-
Notifications
You must be signed in to change notification settings - Fork 456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow Customizing Validation Errors #1380
Conversation
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) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking of exposing this on Routers or api_operations instead, but since exception handlers must be registered on APIs, I couldn't think of a reasonable way to do so.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I preserved all of this behavior by moving it to validation_error_from_error_contexts
in main.py
@@ -28,6 +30,22 @@ class ConfigError(Exception): | |||
pass | |||
|
|||
|
|||
TModel = TypeVar("TModel", bound="ParamModel") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a copy of TModel
in ninja.params.models
. I couldn't figure out a way to import it without causing cyclical import issues, because it's used below in class ValidationErrorContext(Generic[TModel])
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a good place to use a @dataclass
, but I think this better matches the rest of the code style here.
Summary
ninja.errors.ValidationError
s have some fundamental bugs and discard crucial error context (see #1381). I've been working around these problems with monkeypatching in my own project.This PR allows developers to access the missing error context in order to make much friendlier error messages than is currently possible with Django Ninja. It solves Problem 1 from #1381 and partially addresses Problem 2.
Fixing the other problems I found will take more PRs and a larger discussion, but I think this PR makes the situation better.
Before
Prior to this PR, users had access to
ninja.errors.ValidationError
s via@api.exception_handler(ValidationError)
, but this has some problems:model.__pydantic_core_schema__
)loc
field in pydantic errors, which makes it impossible for downstream code to understand what each element of theloc
meansloc
sAfter
This PR allows users to control how
pydantic.ValidationError
s get turned intoninja.errors.ValidationError
s, without breaking backwards compatibility with existing user code.The PR:
ValidationErrorContext
validation_error_from_error_contexts(self, error_contexts: List[ValidationErrorContext]) -> ValidationError
Context
Consider the following example:
If you POST
{ "user": {} }
to/demo/hello
(you can use "Try it out" at "/demo/docs"), the response (with whitespace changes for readability) is:Note the confusing 4th value of each
"loc"
field:"str"
or"function-wrap[_run_root_validator()]"
.Note also that the 2nd value of each
"loc"
field is"data"
, which is the name of a parameter to thehello
function.These parts of
"loc"
are not meaningful to API callers and are brittle in the face of future changes (for example, they will change if thedata
function parameter gets renamed or ifUserSchema
gets a newmodel_validator
).A developer using Django Ninja should be able to produce user-friendly errors like the following instead of the errors in the example above:
This is usually possible with Pydantic by walking
model.__pydantic_core_schema__
with the errorloc
(see below), but Django Ninja discardsmodel.__pydantic_core_schema__
and transformsloc
, so it's not possible in Django Ninja without user control of what makes it intoValidationError
.Understanding Pydantic Errors and
model.__pydantic_core_schema__
In Pydantic 2, the errors in
e.errors
relate not to the input data (the end-user's perspective) but to the schema against which the input data was validated.To construct user-friendly errors, we can crawl the tree rooted in
model.__pydantic_core_schema__
using the original"loc"
field of each error.This allows us to, for example, identify whether a given
loc
element refers to a field, a dictionary key, a union discriminator, or something else.For the
hello
endpoint in Example 1 above, themodel.__pydantic_core_schema__
of the body is:and the first
e.errors()
error is:We can see that
'str'
isn't a field here because it's a discriminator for this'union'
schema:Moreover, we can see that the 3 other errors refer to a different union variant because the 3rd element of
"loc"
is'function-wrap[_run_root_validator()]'
rather than'str'
: