Problem/Motivation

The JSON API specifies that a source pointer may be added to error responses to specify a route in the response JSON to the source of the error in the request. (See http://jsonapi.org/format/#errors ) This would be particularly helpful for validation errors, to indicate in a machine-readable way where the validation failed. Ember.js, for example, utilizes this pointer out-of-the-box to make an error specific to the model's attribute. Not having this pointer specified at all in the response causes the error to not be displayed at all in Ember.js templates.

For example, attempting to update a node without specifying a Title:

{
    "data": {
        "id": "aadca889-b307-421e-83f0-9bc9df3e56c8",
        "attributes": {
            "nid": "5",
            "uuid": "aadca889-b307-421e-83f0-9bc9df3e56c8",
            "created": "1479845577",
            "title": "",
            "body": {
                "value": "The body...",
                "format": "basic_html",
                "summary": "The summary."
            }
        },
        "relationships": {
            "uid": {
                "data": {
                    "type": "user--users",
                    "id": "349f9a3e-74bd-4782-b6fa-edd19d6f129c"
                }
            }
        },
        "type": "node--articles"
    }
}

would produce a response like:

{
    "errors": [{
        "title": "Unprocessable Entity",
        "status": 422,
        "detail": "Unprocessable Entity: validation failed.\ntitle: This value should not be null.\n",
        "source": {
            "pointer": "/data/attributes/title"
        },
        "links": {
            "info": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html"
        },
        "code": 0
    }]
}

Specifying an invalid text format for the body field would produce a pointer of /data/attributes/body/format. A pointer of /data can be used to indicate an error without a known, specific source.

Proposed resolution

Add a subclass, UnprocessableEntityException that extends \Drupal\jsonapi\Error\SerializableHttpException and holds the validation failures. Delegate normalization/serialization of the error response to SerializableHttpException and UnprocessableEntityException. If more than one validation error occurs, use the non-specific /data pointer. (Alternately, we could create a separate error object in the response's "errors" array for each validation failure.)

Remaining tasks

- Fix TODOs
- Tidy-up patch
- Add/fix tests

User interface changes

None

API changes

TBD

Data model changes

None

Comments

hampercm created an issue. See original summary.

hampercm’s picture

StatusFileSize
new6.96 KB

This somewhat quick-and-dirty patch mostly implements the feature, with some TODOs. Any feedback is welcome.

hampercm’s picture

Issue summary: View changes
e0ipso’s picture

Thanks for the patch. This will really help consumers to get meaningful server-side form validation.

  1. +++ b/src/Error/SerializableHttpException.php
    @@ -3,10 +3,42 @@
    +  public function serialize() {
    

    This is the object to serialize. It should not know how to serialize itself. Instead, the serializer should be the one to do that.

  2. +++ b/src/Error/UnprocessableEntityException.php
    @@ -0,0 +1,50 @@
    +class UnprocessableEntityException extends SerializableHttpException {
    

    Maybe UnprocessableHttpEntityException to help avoid confusion?

  3. +++ b/src/Error/UnprocessableEntityException.php
    @@ -0,0 +1,50 @@
    +  public function __construct(EntityConstraintViolationListInterface $violations, \Exception $previous = null, array $headers = array(), $code = 0) {
    

    I'd rather have an addViolation($violation) method in the class than passing the array to the constructor.

  4. +++ b/src/Normalizer/HttpExceptionNormalizer.php
    +++ b/src/Normalizer/HttpExceptionNormalizer.php
    @@ -41,30 +41,42 @@ class HttpExceptionNormalizer extends SerializationNormalizerBase {
    

    I'd like to have a normalizer that takes UnprocessableHttpEntityException and inherits from HttpExceptionNormalizer to add the pointer.

  5. +++ b/src/Normalizer/HttpExceptionNormalizer.php
    @@ -41,30 +41,42 @@ class HttpExceptionNormalizer extends SerializationNormalizerBase {
    +      $error = $object->serialize();
    

    As stated before, let's bring the logic back to this class.

e0ipso’s picture

Status: Active » Needs work
hampercm’s picture

Assigned: Unassigned » hampercm

Thanks for the review. Working on an updated patch...

hampercm’s picture

Assigned: hampercm » Unassigned
Status: Needs work » Needs review
StatusFileSize
new9.37 KB

- Addressed comment #4.
- The patch now creates a separate JSON API error object for each constraint violation. Each error object has a source pointer for the associated field, if applicable.
- I fixed the issue with the first patch where single-value fields would have a 0 index specified in the source pointer (ex. /data/attributes/body/0/value). Now, single-value fields don't have any index (ex. /data/attributes/body/value), to match the JSON API request's object structure.

hampercm’s picture

hampercm’s picture

Status: Needs review » Needs work
hampercm’s picture

Status: Needs work » Needs review

Kicking off automated tests, which didn't run...

e0ipso’s picture

This is definitely in the good direction! I really like this new feature.

A couple more comments on the current implementation:

  1. +++ b/src/Error/UnprocessableHttpEntityException.php
    @@ -0,0 +1,47 @@
    +class UnprocessableHttpEntityException extends SerializableHttpException {
    

    Let's add a short class comment explaining when this exception comes into play.

  2. +++ b/src/Normalizer/HttpExceptionNormalizer.php
    @@ -40,37 +40,56 @@ class HttpExceptionNormalizer extends SerializationNormalizerBase {
    +      return new FieldItemNormalizerValue($error);
    

    Should this be:

    return new FieldItemNormalizerValue([$error]);
    

    I'm unclear on the specifics at the moment, but that is what the previous code seems to be doing.

  3. +++ b/src/Normalizer/HttpExceptionNormalizer.php
    @@ -40,37 +40,56 @@ class HttpExceptionNormalizer extends SerializationNormalizerBase {
    +   * @param object $exception
    +   *  The Exception.
    +   *
    +   * @return array
    +   *  The error objects to include in the response.
    +   */
    +  protected function buildErrorObjects($exception) {
    +    /** @var $exception \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface */
    

    Let's add type hinting to the $exception argument.

  4. +++ b/src/Normalizer/UnprocessableHttpEntityExceptionNormalizer.php
    @@ -0,0 +1,74 @@
    +    unset($error['links']);
    

    Why are you removing this?

  5. +++ b/src/Normalizer/UnprocessableHttpEntityExceptionNormalizer.php
    @@ -0,0 +1,74 @@
    +        $pointer = '/data/attributes/'
    +          . str_replace('.', '/', $violation->getPropertyPath());
    +        if ($cardinality == 1) {
    +          $pointer = str_replace('/0/', '/', $pointer);
    +        }
    

    Is there any way ew can ensure not to remove additional /0/ that we're not supposed to remove.

e0ipso’s picture

We will need testing for this, to make sure that we adhere to the expectations from the front end libraries.

Also, can you please post the response you get to a serialization error with this patch applied with different user roles? I'm trying to make sure we are not leaking any sensitive information.

hampercm’s picture

Assigned: Unassigned » hampercm
Status: Needs review » Needs work
hampercm’s picture

StatusFileSize
new10.66 KB

Thanks for the review. I (mostly) addressed comment #11:
1. Done. Also added PHPDoc for the HttpExceptionNormalizer class.

2. Good catch. Interestingly, the response output looks the same either way, but I'll revert to be safe.

3. Done.

4. The link doesn't provide any relevant information in this case. The 422 error code is instead defined in the WebDAV spec at https://tools.ietf.org/html/rfc4918#page-78 and unofficially repurposed for this sort of thing. I could link to that standard instead, though having a link to a WebDAV spec might be a bit confusing. Much more relevant information is supplied by the error's title and details, so I'm not sure this link is needed. Thoughts?

5. I've improved the code to make is less likely to wipe out '/0/'s that we don't want removed.

Working on tests...

hampercm’s picture

StatusFileSize
new10.96 KB
new4.25 KB

This patch fixes a bug in #14, which caused HttpExceptionNormalizer to not be used, due to an incorrect $supportedInterfaceOrClass. Also adding an interdiff from the patch before @e0ipso's review.

I'm surprised automated tests didn't catch this regression. We should verify that tests for 403 errors exist, and that the JSON response is verified in those cases.

e0ipso’s picture

@hampercm it seems that we only have functional tests on 403 for write operations. We have, however 403 tests for partial success operations (// 12. Collection with one access denied).

I agree that we should add more testing. Do you want to add that here, or in a follow up?

hampercm’s picture

I'll add tests for this patch's new functionality, only. Lets put the 403 tests in a separate issue, or a more general Plan to expand tests.

e0ipso’s picture

That sounds good. I think that test coverage is not in bad shape, so a Plan sounds like too much. We probably want just a new ticket.

hampercm’s picture

StatusFileSize
new13.23 KB
new2.26 KB

Added tests for the source pointer functionality.

hampercm’s picture

Assigned: hampercm » Unassigned
Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 19: add_source_pointer-2832211-19.patch, failed testing.

e0ipso’s picture

Status: Needs work » Needs review
e0ipso’s picture

Status: Needs review » Fixed

I think this is a great DX improvement! Merging.

  • e0ipso committed b2e95ec on 8.x-1.x authored by hampercm
    fix(Serialization) Add source pointer element to 422 responses (#2832211...
hampercm’s picture

Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

rpayanm’s picture

Project: JSON:API » Drupal core
Version: 8.x-1.x-dev » 8.9.x-dev
Component: Code » jsonapi.module

Moving to Drupal core's issue queue.

I'm working on https://www.drupal.org/project/drupal/issues/3122113