-
-
Notifications
You must be signed in to change notification settings - Fork 7k
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
Conversation
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' |
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.
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.
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.
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.
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.
(Because easiest to review impact pros/cons of each when considered in isolation)
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.
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.
Nice addition. |
@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. |
Noted, thanks. |
I would love to see this merged. |
@johnraz Believe it's pending review after that. |
@qsorix : do you mind rebasing ? |
@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. |
@qsorix : awesome ! |
b5e4755
to
d7ca008
Compare
@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. |
👍 for usefullness of such a patch. |
@tomchristie who is in charge of the review? yourself? |
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! 😄 |
ack. If I can be of any help with this, shoot. |
I guess the only thing here that I'm not so sure about(or perhaps don't understand the reason behind) is the |
@jpadilla the way I see it, such change is only useful, if all errors will include an error. The 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) |
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 think this should be:
exceptions.build_error_from_django_validation_error(exc)
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.
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.
d7ca008
to
4e634ca
Compare
@johnraz, fixed. Thanks for reviewing so carefully! |
@qsorix I just gave it a try and noticed some issue, it's always better to get review from others. |
Ok now I have a more detailed question. It's not quite clear how you intend to monkey patch the 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 ? |
I don't quite have time for detailed review on this right now, apologies. |
@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 ;-) |
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. 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 I introduced the 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.
One more thing to keep in mind, is that the result of 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 |
+1 |
@qsorix : I think that this PR is still out of the scope of what @tomchristie wanted in #3137
And this PR:
Versus #3137:
They are about the same. I think that @tomchristie was not against using the settings to store the 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 What do you think ? |
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. |
@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. |
@johnraz Correct - this still looks a little out of scope. The |
👍 |
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.
I opened a new PR #3716, to try to push things forward. |
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.
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.
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.
@tomchristie @qsorix I'd recommend closing this in favor of #3775 to reduce the noise and get along with things more easily. |
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.
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.