Skip to content

Add error codes to ValidationError #3169

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

Closed

Conversation

qsorix
Copy link

@qsorix qsorix commented Jul 17, 2015

This change addresses use cases that require more information about reported
validation errors. Currently for each error that REST Framework reports users
get only that error's message string. The message can be translated so there's
no good way to recognize programmatically what sort of an error it is.

When building an API that is supposed to return error codes, I've found it very
limiting. For example, I was supposed to differentiate between missing fields
and invalid arguments.

This commit introduces proper error codes handling to the ValidationError.

ValidationError can hold a single error itself (text), a list of those, or a
dictionary mapping errors to fields. Error code is only meaningful for a single
error, and I've added assertions to check for proper usage.

To help with my development, I've added a setting that makes error code a
mandatory argument. Thanks to this, I was able to correct all uses of
ValidationError across the code.

To maintain backward compatibility, I'm not passing error codes when building
compound errors (e.g. a dictionary with all validation errors). However, users
(me) can now monkey patch ValidationError.build_detail method, to store the
codes.

@qsorix
Copy link
Author

qsorix commented Jul 17, 2015

Here's a simplified approach to solve issue #3111.

Compared to the previous attempt (#3137) I got rid of 2 levels of indirection caused by the error_builder that could be customized via settings. Instead now there's a single static method that is easy to understand, so it doesn't add much of maintenance burden, and also provides a customization point for those few users (me :P) that actually need this.

Waiting for feedback. Thanks.

raise exceptions.ValidationError(msg)
raise exceptions.ValidationError(
msg,
code='authorization'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of these strings might be common beyond DRF's internals. For example, I might want to use a 'authorisation' error code in my own code. Reckon we could make them into constants? A la the status code constants. It might be easier for translation that way too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not overly keen but open to discussion. However let's keep that seperate to this - I'd suggest opening it as an incremental improvement if and when we get this agreed & merged.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Because easiest to review impact pros/cons of each when considered in isolation)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another solution would be to make the codes specific to the type of exception. eg.: "validation_error.authorization". This will also help clients that consume this API to unequivocally identify error codes/keys.

@JlUgia
Copy link

JlUgia commented Jul 29, 2015

Nice addition.
Why not elevating this to APIException? There are other errors that could benefit from this approach like ParseError or PermissionDenied. Also that would avoid inconsistencies of some details being plain text and others whichever format is implemented in build_detail.

@tomchristie
Copy link
Member

@JlUgia Not convinced, but in either case I'd say consider that separately. There's plenty here already that'd need to get past the review stage, so let's not entangle that with further complications at this point.

@JlUgia
Copy link

JlUgia commented Jul 29, 2015

Noted, thanks.
I'll keep track of this and prototype to assess usefulness as soon as this is included.

@johnraz
Copy link
Contributor

johnraz commented Oct 5, 2015

I would love to see this merged.
Is there anything preventing the merge apart from the obvious rebase + conflict resolution?

@tomchristie
Copy link
Member

@johnraz Believe it's pending review after that.

@johnraz
Copy link
Contributor

johnraz commented Oct 6, 2015

@qsorix : do you mind rebasing ?

@qsorix
Copy link
Author

qsorix commented Oct 12, 2015

@johnraz, sorry for a late response. I was enjoying holidays.

I'll try to work on the rebase this week, once I take care of other things in my queue.

@johnraz
Copy link
Contributor

johnraz commented Oct 12, 2015

@qsorix : awesome !

@qsorix qsorix force-pushed the error-codes-enhancement-scope-reduced branch from b5e4755 to d7ca008 Compare October 23, 2015 22:10
@qsorix
Copy link
Author

qsorix commented Oct 23, 2015

@johnraz, @tomchristie branch rebased. Let me know if I can help/continue in any other way. Sorry for the delay, I'm quite busy these days. Cheers.

@JocelynDelalande
Copy link
Contributor

👍 for usefullness of such a patch.

@johnraz
Copy link
Contributor

johnraz commented Oct 28, 2015

@tomchristie who is in charge of the review? yourself?
The only thing that I see as missing is the documentation.

@tomchristie
Copy link
Member

Anyone on the maintainance team. We've been stacked getting 3.3 out, and there's more of a backlog than normal right now. We'll get there tho! 😄

@johnraz
Copy link
Contributor

johnraz commented Nov 5, 2015

ack. If I can be of any help with this, shoot.
I have a ticket in my day job sprint regarding this, so I'll have to dig it anyway.

@jpadilla
Copy link
Member

jpadilla commented Nov 5, 2015

I guess the only thing here that I'm not so sure about(or perhaps don't understand the reason behind) is the REQUIRE_ERROR_CODES setting.

@qsorix
Copy link
Author

qsorix commented Nov 5, 2015

@jpadilla the way I see it, such change is only useful, if all errors will include an error. The REQUIRE_ERROR_CODES is supposed to help achieve that in development.

What I did when working on this, was setting this to true and running unit tests, correcting them until all passed without complaining. This way I had some confidence that I've added error codes throughout all REST framework's code.

I'm not convinced that an option is settings is the right way to do it, but as a developer, I certainly liked having those assertions.

Unless I misunderstood, and you just want to enable those warnings all the time? In such case, I've added it to keep the change backward compatible. Users may have some custom error classes.

@@ -440,7 +443,9 @@ def to_internal_value(self, data):
except ValidationError as exc:
errors[field.field_name] = exc.detail
except DjangoValidationError as exc:
errors[field.field_name] = list(exc.messages)
errors[field.field_name] = (
exceptions.build_error_from_django_validation_error(exc.messages)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be:

exceptions.build_error_from_django_validation_error(exc)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. Looks like I'm missing a test case that covers this path. I'll cover this and push an update. Hopefully tomorrow :)

This change addresses use cases that require more information about reported
validation errors. Currently for each error that REST Framework reports users
get only that error's message string. The message can be translated so there's
no good way to recognize programmatically what sort of an error it is.

When building an API that is supposed to return error codes, I've found it very
limiting. For example, I was supposed to differentiate between missing fields
and invalid arguments.

This commit introduces proper error codes handling to the ValidationError.

ValidationError can hold a single error itself (text), a list of those, or a
dictionary mapping errors to fields. Error code is only meaningful for a single
error, and I've added assertions to check for proper usage.

To help with my development, I've added a setting that makes error code a
mandatory argument. Thanks to this, I was able to correct all uses of
ValidationError across the code.

To maintain backward compatibility, I'm not passing error codes when building
compound errors (e.g. a dictionary with all validation errors). However, users
(me) can now monkey patch ValidationError.build_detail method, to store the
codes.
@qsorix qsorix force-pushed the error-codes-enhancement-scope-reduced branch from d7ca008 to 4e634ca Compare November 9, 2015 19:23
@qsorix
Copy link
Author

qsorix commented Nov 9, 2015

@johnraz, fixed. Thanks for reviewing so carefully!

@johnraz
Copy link
Contributor

johnraz commented Nov 9, 2015

@qsorix I just gave it a try and noticed some issue, it's always better to get review from others.
Glad I could be of any help anyway.

@johnraz
Copy link
Contributor

johnraz commented Nov 10, 2015

Ok now I have a more detailed question.

It's not quite clear how you intend to monkey patch the build_detail method.
Can you go a bit more in depth about that?
Also I guess the method is declared as static because of that?

Why don't you re-use the same mechanism used for the custom exception handler as done here

I think it would be much more consistent with the rest of the system as I see the validation error customization as a complement to the exception handler customization.

@tomchristie @jpadilla any thoughts ?

@tomchristie
Copy link
Member

@tomchristie @jpadilla any thoughts?

I don't quite have time for detailed review on this right now, apologies.

@johnraz
Copy link
Contributor

johnraz commented Nov 10, 2015

@tomchristie no problem, I need this quite quickly and will probably maintain a fork meanwhile anyway.

I will keep talking with @qsorix and probably come up with some documentation at some point in order to ease the review time ;-)

@qsorix
Copy link
Author

qsorix commented Nov 10, 2015

@johnraz

Why don't you re-use the same mechanism used for the custom exception handler as done here
I think it would be much more consistent with the rest of the system as I see the validation error customization as a complement to the exception handler customization.

Initially I've issued a pull request where this behavior was customized in the same way, i.e. via settings. Tom Christie decided to limit the change, to make review easier.
You can see the old code and discussion here #3137. qsorix@aa4421c#diff-7215000329f5ee2146a1d6bdcdf270b3R62

I agreed and then thought how to limit the scope.

Because validation errors are grouped into a tree (of all validation errors), beside obvious instantiation places, I had to change a few more lines in the code. Also, please note that when building a tree of errors, only exc.detail is propagated, not the whole ValidationError. That's why I couldn't only set the code as a member.

I introduced the build_detail method. Its only purpose is to be a single place that defines what is inside of exc.detail.

A single static method seemed like the cleanest thing to do. It doesn't obscure the code, and it has a single responsibility, so it is easy to monkey patch and unlikely to cause problems when upgrading.

It's not quite clear how you intend to monkey patch the build_detail method.
Can you go a bit more in depth about that?

One more thing to keep in mind, is that the result of build_detail is only used at the leaves of the error tree. Above that we have lists and dictionaries that together form the structure holding all validation errors. By changing implementation of build_detail, we control how leaves of that tree look like.

As you see, the default version simply drops the error code:

@staticmethod
def build_detail(detail, code=None):
    return detail

And your serializer.errors can look like this:

{'integer': ['This field is required.']}

By default it's an identity function -- to retain backward compatibility. But not very useful if you want error codes. On my testing branch, where I make sure this pull request works, I'm changing it to this:

def build_detail(detail, code=None):
    return (detail, code)

And now that same error will become:

{'integer': [('This field is required.', 'required')]}

For production code, I suggest e.g. a named tuple. And that's it. The rest goes in the exception handler as it does today.

The solution with build_detail is more complicated than it could be if we weren't concerned about backward compatibility. The easiest thing to would be to just keep ValidationError instances in the leaves of the tree.

@sennav
Copy link

sennav commented Nov 19, 2015

+1

@johnraz
Copy link
Contributor

johnraz commented Nov 24, 2015

@qsorix : I think that this PR is still out of the scope of what @tomchristie wanted in #3137

I would restrict the change solely to this:

"Add optional .code descriptions to ValidationError"

No build_error functions, and no different handling of ValidationError by default - just the most minimal possible pull request to support the change described above.

It's unclear how we'd handle nested and lists of messages. Either we could do that by having the code attached as a property on the message, or .code could be a nested structure.

It's also not a given that we'd accept that pull request, but it would be viable, and it'd be much easier to assess.

In it's current state I'm closing this as too-broad ranging. Any future iteration which solely targets introducing .code on ValidationError or introducing .code on ValidationError.message may be acceptable.

And this PR:

Showing 11 changed files with 161 additions and 22 deletions.

Versus #3137:

Showing 11 changed files with 166 additions and 23 deletions.

They are about the same. I think that @tomchristie was not against using the settings to store the build_detail method override but more asking to have a simpler version of the change.

And personally I think the settings solution was way more clean than a monkey patch solution.

What we want here is to get this merged, so I think we should be able to restrict a bit more what this PR is doing in order to get it merged, and come up with another one that goes a bit deeper.

Probably get rid of any "customization" method for now, so remove the build_detail method, even if it makes plain sense to me, get the code part merged and move on to another PR to get build_detail merged.

What do you think ?

@qsorix
Copy link
Author

qsorix commented Nov 24, 2015

@johnraz

IMHO splitting this in half and merging first part only puts REST framework in a position when there's more code in place and at the same time that code is useless. It's always possible that the other half will not get approved. So the first part would only make REST framework's code worse.

Personally, I would not accept such "limited-use" merge request and since I got no clear message that this is the right way to get the whole change approved, I did not went that path.

Of course feel free to split it and create a different pull request. You can even copy my changes, I don't mind. This PR is not merged anyway so an alternative approach is justified. I'm sorry, but for the next 2 months I won't have time to work on this myself.

@johnraz
Copy link
Contributor

johnraz commented Nov 24, 2015

@qsorix I fully agree. I'm just trying to push this forward.

@tomchristie : can you give us a hint that we are going in the right direction here, I'm really wanting to make this happen, I just would like to avoid useless work if possible.

@tomchristie
Copy link
Member

@johnraz Correct - this still looks a little out of scope. The build_error_from_django_validation_error and the REQUIRE_ERROR_CODES setting remain problematic. What I'd like to see is purely the error codes PR. That PR could then be presented along with notes on what further work a developer would still need to do in order to present custom error responses to the end user.

@diegobill
Copy link

👍

johnraz added a commit to johnraz/django-rest-framework that referenced this pull request Dec 8, 2015
This patch is meant to fix encode#3111, regarding comments made to encode#3137
and encode#3169.

The `ValidationError` will now contain a `code` attribute.

Before this patch, `ValidationError.detail` only contained a
`dict` with values equal to a  `list` of string error messages or
directly a `list` containing string error messages.

Now, the string error messages are replaced with `ValidationError`.

This means that, depending on the case, you will not only get a
string back but a all object containing both the error message and
the error code, respectively `ValidationError.detail` and
`ValidationError.code`.

It is important to note that the `code` attribute is not relevant
when the `ValidationError` represents a combination of errors and
hence is `None` in such cases.

The main benefit of this change is that the error message and
error code are now accessible the custom exception handler and can
be used to format the error response.

An custom exception handler example is available in the
`TestValidationErrorWithCode` test.
@johnraz
Copy link
Contributor

johnraz commented Dec 8, 2015

I opened a new PR #3716, to try to push things forward.

johnraz added a commit to johnraz/django-rest-framework that referenced this pull request Dec 8, 2015
This patch is meant to fix encode#3111, regarding comments made to encode#3137
and encode#3169.

The `ValidationError` will now contain a `code` attribute.

Before this patch, `ValidationError.detail` only contained a
`dict` with values equal to a  `list` of string error messages or
directly a `list` containing string error messages.

Now, the string error messages are replaced with `ValidationError`.

This means that, depending on the case, you will not only get a
string back but a all object containing both the error message and
the error code, respectively `ValidationError.detail` and
`ValidationError.code`.

It is important to note that the `code` attribute is not relevant
when the `ValidationError` represents a combination of errors and
hence is `None` in such cases.

The main benefit of this change is that the error message and
error code are now accessible the custom exception handler and can
be used to format the error response.

An custom exception handler example is available in the
`TestValidationErrorWithCode` test.
johnraz added a commit to johnraz/django-rest-framework that referenced this pull request Feb 23, 2016
This patch is meant to fix encode#3111, regarding comments made to encode#3137
and encode#3169.

The `ValidationError` will now contain a `code` attribute.

Before this patch, `ValidationError.detail` only contained a
`dict` with values equal to a  `list` of string error messages or
directly a `list` containing string error messages.

Now, the string error messages are replaced with `ValidationError`.

This means that, depending on the case, you will not only get a
string back but a all object containing both the error message and
the error code, respectively `ValidationError.detail` and
`ValidationError.code`.

It is important to note that the `code` attribute is not relevant
when the `ValidationError` represents a combination of errors and
hence is `None` in such cases.

The main benefit of this change is that the error message and
error code are now accessible the custom exception handler and can
be used to format the error response.

An custom exception handler example is available in the
`TestValidationErrorWithCode` test.
johnraz added a commit to johnraz/django-rest-framework that referenced this pull request Apr 7, 2016
This patch is meant to fix encode#3111, regarding comments made to encode#3137
and encode#3169.

The `ValidationError` will now contain a `code` attribute.

Before this patch, `ValidationError.detail` only contained a
`dict` with values equal to a  `list` of string error messages or
directly a `list` containing string error messages.

Now, the string error messages are replaced with `ValidationError`.

This means that, depending on the case, you will not only get a
string back but a all object containing both the error message and
the error code, respectively `ValidationError.detail` and
`ValidationError.code`.

It is important to note that the `code` attribute is not relevant
when the `ValidationError` represents a combination of errors and
hence is `None` in such cases.

The main benefit of this change is that the error message and
error code are now accessible the custom exception handler and can
be used to format the error response.

An custom exception handler example is available in the
`TestValidationErrorWithCode` test.
@johnraz
Copy link
Contributor

johnraz commented Apr 12, 2016

@tomchristie @qsorix I'd recommend closing this in favor of #3775 to reduce the noise and get along with things more easily.

johnraz added a commit to johnraz/django-rest-framework that referenced this pull request Aug 28, 2016
This patch is meant to fix encode#3111, regarding comments made to encode#3137
and encode#3169.

The `ValidationError` will now contain a `code` attribute.

Before this patch, `ValidationError.detail` only contained a
`dict` with values equal to a  `list` of string error messages or
directly a `list` containing string error messages.

Now, the string error messages are replaced with `ValidationError`.

This means that, depending on the case, you will not only get a
string back but a all object containing both the error message and
the error code, respectively `ValidationError.detail` and
`ValidationError.code`.

It is important to note that the `code` attribute is not relevant
when the `ValidationError` represents a combination of errors and
hence is `None` in such cases.

The main benefit of this change is that the error message and
error code are now accessible the custom exception handler and can
be used to format the error response.

An custom exception handler example is available in the
`TestValidationErrorWithCode` test.

We keep `Serializer.errors`'s return type unchanged in
order to maintain backward compatibility.

The error codes will only be propagated to the `exception_handler`
or accessible through the `Serializer._errors` private attribute.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy