Skip to content

Commit acc6bfc

Browse files
authored
fix(jsonapi): add status and title, fix pointer (#8755)
Fixes #8748
1 parent a446a4f commit acc6bfc

5 files changed

Lines changed: 97 additions & 10 deletions

File tree

‎src/JsonApi/Serializer/ConstraintViolationListNormalizer.php‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\JsonApi\Serializer;
1515

16+
use ApiPlatform\Metadata\HttpOperation;
1617
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
1718
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
1819
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -38,9 +39,13 @@ public function __construct(private readonly PropertyMetadataFactoryInterface $p
3839
*/
3940
public function normalize(mixed $data, ?string $format = null, array $context = []): array
4041
{
42+
$operation = $context['operation'] ?? null;
43+
$status = (string) ($operation instanceof HttpOperation ? $operation->getStatus() : null) ?: '422';
4144
$violations = [];
4245
foreach ($data as $violation) {
4346
$violations[] = [
47+
'status' => $status,
48+
'title' => 'Validation Failed',
4449
'detail' => $violation->getMessage(),
4550
'source' => [
4651
'pointer' => $this->getSourcePointerFromViolation($violation),
@@ -72,13 +77,13 @@ private function getSourcePointerFromViolation(ConstraintViolationInterface $vio
7277
$fieldName = $violation->getPropertyPath();
7378

7479
if (!$fieldName) {
75-
return 'data';
80+
return '/data';
7681
}
7782

7883
$root = $violation->getRoot();
7984

8085
if (!\is_object($root)) {
81-
return "data/attributes/$fieldName";
86+
return "/data/attributes/$fieldName";
8287
}
8388

8489
$class = $root::class;
@@ -94,9 +99,9 @@ private function getSourcePointerFromViolation(ConstraintViolationInterface $vio
9499
}
95100

96101
if ($propertyMetadata->getNativeType()?->isSatisfiedBy(static fn ($t) => $t instanceof ObjectType)) {
97-
return "data/relationships/$fieldName";
102+
return "/data/relationships/$fieldName";
98103
}
99104

100-
return "data/attributes/$fieldName";
105+
return "/data/attributes/$fieldName";
101106
}
102107
}

‎src/JsonApi/Tests/Serializer/ConstraintViolationNormalizerTest.php‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,21 +71,27 @@ public function testNormalize(): void
7171
[
7272
'errors' => [
7373
[
74+
'status' => '422',
75+
'title' => 'Validation Failed',
7476
'detail' => 'This value should not be null.',
7577
'source' => [
76-
'pointer' => 'data/relationships/relatedDummy',
78+
'pointer' => '/data/relationships/relatedDummy',
7779
],
7880
],
7981
[
82+
'status' => '422',
83+
'title' => 'Validation Failed',
8084
'detail' => 'This value should not be null.',
8185
'source' => [
82-
'pointer' => 'data/attributes/name',
86+
'pointer' => '/data/attributes/name',
8387
],
8488
],
8589
[
90+
'status' => '422',
91+
'title' => 'Validation Failed',
8692
'detail' => 'Unknown violation.',
8793
'source' => [
88-
'pointer' => 'data',
94+
'pointer' => '/data',
8995
],
9096
],
9197
],
@@ -111,9 +117,11 @@ public function testNormalizeWithStringRoot(): void
111117
[
112118
'errors' => [
113119
[
120+
'status' => '422',
121+
'title' => 'Validation Failed',
114122
'detail' => 'Invalid page value.',
115123
'source' => [
116-
'pointer' => 'data/attributes/page',
124+
'pointer' => '/data/attributes/page',
117125
],
118126
],
119127
],
@@ -140,9 +148,11 @@ public function testNormalizeWithNullRoot(): void
140148
[
141149
'errors' => [
142150
[
151+
'status' => '422',
152+
'title' => 'Validation Failed',
143153
'detail' => 'Invalid value.',
144154
'source' => [
145-
'pointer' => 'data/attributes/field',
155+
'pointer' => '/data/attributes/field',
146156
],
147157
],
148158
],

‎tests/Fixtures/TestBundle/ApiResource/JsonApi/ErrorProblem.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
uriTemplate: '/jsonapi_validation_problem',
2929
processor: [self::class, 'processValidation'],
3030
),
31+
new Post(
32+
uriTemplate: '/jsonapi_validation_problem_bad_request',
33+
processor: [self::class, 'processValidation'],
34+
exceptionToStatus: [ValidationException::class => 400],
35+
),
3136
new Post(
3237
uriTemplate: '/jsonapi_exception_problem',
3338
processor: [self::class, 'processBadRequest'],
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Functional\JsonApi;
15+
16+
use ApiPlatform\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonApi\ErrorProblem;
18+
use ApiPlatform\Tests\SetupClassResourcesTrait;
19+
20+
final class ErrorObjectMembersTest extends ApiTestCase
21+
{
22+
use SetupClassResourcesTrait;
23+
24+
protected static ?bool $alwaysBootKernel = false;
25+
26+
public static function getResources(): array
27+
{
28+
return [ErrorProblem::class];
29+
}
30+
31+
public function testValidationErrorHasStatusAndTitle(): void
32+
{
33+
$response = self::createClient()->request('POST', '/jsonapi_validation_problem', [
34+
'headers' => ['accept' => 'application/vnd.api+json', 'content-type' => 'application/vnd.api+json'],
35+
'json' => ['data' => ['type' => 'JsonApiErrorProblem', 'attributes' => new \stdClass()]],
36+
]);
37+
38+
$this->assertResponseStatusCodeSame(422);
39+
$error = $response->toArray(false)['errors'][0];
40+
$this->assertSame('422', $error['status'] ?? null);
41+
$this->assertArrayHasKey('title', $error);
42+
}
43+
44+
public function testValidationErrorPointerIsAJsonPointer(): void
45+
{
46+
$response = self::createClient()->request('POST', '/jsonapi_validation_problem', [
47+
'headers' => ['accept' => 'application/vnd.api+json', 'content-type' => 'application/vnd.api+json'],
48+
'json' => ['data' => ['type' => 'JsonApiErrorProblem', 'attributes' => new \stdClass()]],
49+
]);
50+
51+
$this->assertResponseStatusCodeSame(422);
52+
$this->assertSame('/data/attributes/name', $response->toArray(false)['errors'][0]['source']['pointer']);
53+
}
54+
55+
public function testValidationErrorStatusFollowsTheResponseStatus(): void
56+
{
57+
$response = self::createClient()->request('POST', '/jsonapi_validation_problem_bad_request', [
58+
'headers' => ['accept' => 'application/vnd.api+json', 'content-type' => 'application/vnd.api+json'],
59+
'json' => ['data' => ['type' => 'JsonApiErrorProblem', 'attributes' => new \stdClass()]],
60+
]);
61+
62+
$this->assertResponseStatusCodeSame(400);
63+
$this->assertSame('400', $response->toArray(false)['errors'][0]['status'] ?? null);
64+
}
65+
}

‎tests/Functional/JsonApi/ErrorTest.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ public function testValidationErrorRendersJsonApiPointer(): void
7070
$this->assertJsonEquals([
7171
'errors' => [
7272
[
73+
'status' => '422',
74+
'title' => 'Validation Failed',
7375
'detail' => 'This value should not be blank.',
74-
'source' => ['pointer' => 'data/attributes/name'],
76+
'source' => ['pointer' => '/data/attributes/name'],
7577
],
7678
],
7779
]);

0 commit comments

Comments
 (0)