Skip to content

Commit 10da65f

Browse files
feat: support collect denormalization errors (#5170)
* feat: support collect dernormalization errors * feat: complete collectDenormalizationErrors implementation Co-authored-by: Alan Poulain <[email protected]>
1 parent 902b135 commit 10da65f

37 files changed

+402
-43
lines changed

features/main/relation.feature

+1-1
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ Feature: Relations support
502502
"pattern": "^An error occurred$"
503503
},
504504
"hydra:description": {
505-
"pattern": "^Expected IRI or document for resource \"ApiPlatform\\\\Tests\\\\Fixtures\\\\TestBundle\\\\(Document|Entity)\\\\RelatedDummy\", \"integer\" given.$"
505+
"pattern": "^The type of the \"ApiPlatform\\\\Tests\\\\Fixtures\\\\TestBundle\\\\(Document|Entity)\\\\RelatedDummy\" resource must be \"array\" \\(nested document\\) or \"string\" \\(IRI\\), \"integer\" given.$"
506506
}
507507
},
508508
"required": [

features/main/validation.feature

+67
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,70 @@ Feature: Using validations groups
7272
}
7373
"""
7474
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
75+
76+
@!mongodb
77+
@createSchema
78+
Scenario: Create a resource with collectDenormalizationErrors
79+
When I add "Content-type" header equal to "application/ld+json"
80+
And I send a "POST" request to "/dummy_collect_denormalization" with body:
81+
"""
82+
{
83+
"foo": 3,
84+
"bar": "baz",
85+
"qux": true,
86+
"uuid": "y",
87+
"relatedDummy": 8,
88+
"relatedDummies": 76
89+
}
90+
"""
91+
Then the response status code should be 422
92+
And the response should be in JSON
93+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
94+
And the JSON should be equal to:
95+
"""
96+
{
97+
"@context": "/contexts/ConstraintViolationList",
98+
"@type": "ConstraintViolationList",
99+
"hydra:title": "An error occurred",
100+
"hydra:description": "This value should be of type unknown.\nqux: This value should be of type string.\nfoo: This value should be of type bool.\nbar: This value should be of type int.\nuuid: This value should be of type uuid.\nrelatedDummy: This value should be of type array|string.\nrelatedDummies: This value should be of type array.",
101+
"violations": [
102+
{
103+
"propertyPath": "",
104+
"message": "This value should be of type unknown.",
105+
"code": "0",
106+
"hint": "Failed to create object because the class misses the \"baz\" property."
107+
},
108+
{
109+
"propertyPath": "qux",
110+
"message": "This value should be of type string.",
111+
"code": "0"
112+
},
113+
{
114+
"propertyPath": "foo",
115+
"message": "This value should be of type bool.",
116+
"code": "0"
117+
},
118+
{
119+
"propertyPath": "bar",
120+
"message": "This value should be of type int.",
121+
"code": "0"
122+
},
123+
{
124+
"propertyPath": "uuid",
125+
"message": "This value should be of type uuid.",
126+
"code": "0",
127+
"hint": "Invalid UUID string: y"
128+
},
129+
{
130+
"propertyPath": "relatedDummy",
131+
"message": "This value should be of type array|string.",
132+
"code": "0"
133+
},
134+
{
135+
"propertyPath": "relatedDummies",
136+
"message": "This value should be of type array.",
137+
"code": "0"
138+
}
139+
]
140+
}
141+
"""

features/serializer/vo_relations.feature

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ Feature: Value object as ApiResource
158158
"pattern": "^An error occurred$"
159159
},
160160
"hydra:description": {
161-
"pattern": "^Cannot create an instance of ApiPlatform\\\\Tests\\\\Fixtures\\\\TestBundle\\\\(Document|Entity)\\\\VoDummyCar from serialized data because its constructor requires parameter \"drivers\" to be present.$"
161+
"pattern": "^Cannot create an instance of \"ApiPlatform\\\\Tests\\\\Fixtures\\\\TestBundle\\\\(Document|Entity)\\\\VoDummyCar\" from serialized data because its constructor requires parameter \"drivers\" to be present.$"
162162
}
163163
},
164164
"required": [

src/JsonApi/Serializer/ItemNormalizer.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,12 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v
192192
* @see http://jsonapi.org/format/#document-resource-object-linkage
193193
*
194194
* @throws RuntimeException
195-
* @throws NotNormalizableValueException
195+
* @throws UnexpectedValueException
196196
*/
197197
protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): object
198198
{
199199
if (!\is_array($value) || !isset($value['id'], $value['type'])) {
200-
throw new NotNormalizableValueException('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.');
200+
throw new UnexpectedValueException('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.');
201201
}
202202

203203
try {

src/Metadata/ApiResource.php

+14
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public function __construct(
112112
protected ?array $cacheHeaders = null,
113113
protected ?array $normalizationContext = null,
114114
protected ?array $denormalizationContext = null,
115+
protected ?bool $collectDenormalizationErrors = null,
115116
protected ?array $hydraContext = null,
116117
protected ?array $openapiContext = null, // TODO Remove in 4.0
117118
protected bool|OpenApiOperation|null $openapi = null,
@@ -537,6 +538,19 @@ public function withDenormalizationContext(array $denormalizationContext): self
537538
return $self;
538539
}
539540

541+
public function getCollectDenormalizationErrors(): ?bool
542+
{
543+
return $this->collectDenormalizationErrors;
544+
}
545+
546+
public function withCollectDenormalizationErrors(bool $collectDenormalizationErrors = null): self
547+
{
548+
$self = clone $this;
549+
$self->collectDenormalizationErrors = $collectDenormalizationErrors;
550+
551+
return $self;
552+
}
553+
540554
/**
541555
* @return string[]|null
542556
*/

src/Metadata/Delete.php

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public function __construct(
6868
?string $description = null,
6969
?array $normalizationContext = null,
7070
?array $denormalizationContext = null,
71+
?bool $collectDenormalizationErrors = null,
7172
?string $security = null,
7273
?string $securityMessage = null,
7374
?string $securityPostDenormalize = null,

src/Metadata/Extractor/XmlResourceExtractor.php

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ private function buildBase(\SimpleXMLElement $resource): array
135135
'securityPostValidationMessage' => $this->phpize($resource, 'securityPostValidationMessage', 'string'),
136136
'normalizationContext' => isset($resource->normalizationContext->values) ? $this->buildValues($resource->normalizationContext->values) : null,
137137
'denormalizationContext' => isset($resource->denormalizationContext->values) ? $this->buildValues($resource->denormalizationContext->values) : null,
138+
'collectDenormalizationErrors' => $this->phpize($resource, 'collectDenormalizationErrors', 'bool'),
138139
'validationContext' => isset($resource->validationContext->values) ? $this->buildValues($resource->validationContext->values) : null,
139140
'filters' => $this->buildArrayValue($resource, 'filter'),
140141
'order' => isset($resource->order->values) ? $this->buildValues($resource->order->values) : null,

src/Metadata/Extractor/YamlResourceExtractor.php

+1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ private function buildBase(array $resource): array
159159
'output' => $this->phpize($resource, 'output', 'bool|string'),
160160
'normalizationContext' => $this->buildArrayValue($resource, 'normalizationContext'),
161161
'denormalizationContext' => $this->buildArrayValue($resource, 'denormalizationContext'),
162+
'collectDenormalizationErrors' => $this->phpize($resource, 'collectDenormalizationErrors', 'bool'),
162163
'validationContext' => $this->buildArrayValue($resource, 'validationContext'),
163164
'filters' => $this->buildArrayValue($resource, 'filters'),
164165
'order' => $this->buildArrayValue($resource, 'order'),

src/Metadata/Extractor/schema/resources.xsd

+1
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@
459459
<xsd:attribute type="xsd:string" name="securityPostDenormalizeMessage"/>
460460
<xsd:attribute type="xsd:string" name="securityPostValidation"/>
461461
<xsd:attribute type="xsd:string" name="securityPostValidationMessage"/>
462+
<xsd:attribute type="xsd:boolean" name="collectDenormalizationErrors"/>
462463
</xsd:attributeGroup>
463464

464465
<xsd:attributeGroup name="extendedBase">

src/Metadata/Get.php

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public function __construct(
6868
?string $description = null,
6969
?array $normalizationContext = null,
7070
?array $denormalizationContext = null,
71+
?bool $collectDenormalizationErrors = null,
7172
?string $security = null,
7273
?string $securityMessage = null,
7374
?string $securityPostDenormalize = null,

src/Metadata/GetCollection.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public function __construct(
7070
?string $description = null,
7171
?array $normalizationContext = null,
7272
?array $denormalizationContext = null,
73+
?bool $collectDenormalizationErrors = null,
7374
?string $security = null,
7475
?string $securityMessage = null,
7576
?string $securityPostDenormalize = null,
@@ -100,7 +101,7 @@ public function __construct(
100101
?OptionsInterface $stateOptions = null,
101102
array $extraProperties = [],
102103
) {
103-
parent::__construct(self::METHOD_GET, $uriTemplate, $types, $formats, $inputFormats, $outputFormats, $uriVariables, $routePrefix, $routeName, $defaults, $requirements, $options, $stateless, $sunset, $acceptPatch, $status, $host, $schemes, $condition, $controller, $cacheHeaders, $hydraContext, $openapiContext, $openapi, $exceptionToStatus, $queryParameterValidationEnabled, $shortName, $class, $paginationEnabled, $paginationType, $paginationItemsPerPage, $paginationMaximumItemsPerPage, $paginationPartial, $paginationClientEnabled, $paginationClientItemsPerPage, $paginationClientPartial, $paginationFetchJoinCollection, $paginationUseOutputWalkers, $paginationViaCursor, $order, $description, $normalizationContext, $denormalizationContext, $security, $securityMessage, $securityPostDenormalize, $securityPostDenormalizeMessage, $securityPostValidation, $securityPostValidationMessage, $deprecationReason, $filters, $validationContext, $input, $output, $mercure, $messenger, $elasticsearch, $urlGenerationStrategy, $read, $deserialize, $validate, $write, $serialize, $fetchPartial, $forceEager, $priority, $name, $provider, $processor, $stateOptions, $extraProperties);
104+
parent::__construct(self::METHOD_GET, $uriTemplate, $types, $formats, $inputFormats, $outputFormats, $uriVariables, $routePrefix, $routeName, $defaults, $requirements, $options, $stateless, $sunset, $acceptPatch, $status, $host, $schemes, $condition, $controller, $cacheHeaders, $hydraContext, $openapiContext, $openapi, $exceptionToStatus, $queryParameterValidationEnabled, $shortName, $class, $paginationEnabled, $paginationType, $paginationItemsPerPage, $paginationMaximumItemsPerPage, $paginationPartial, $paginationClientEnabled, $paginationClientItemsPerPage, $paginationClientPartial, $paginationFetchJoinCollection, $paginationUseOutputWalkers, $paginationViaCursor, $order, $description, $normalizationContext, $denormalizationContext, $collectDenormalizationErrors, $security, $securityMessage, $securityPostDenormalize, $securityPostDenormalizeMessage, $securityPostValidation, $securityPostValidationMessage, $deprecationReason, $filters, $validationContext, $input, $output, $mercure, $messenger, $elasticsearch, $urlGenerationStrategy, $read, $deserialize, $validate, $write, $serialize, $fetchPartial, $forceEager, $priority, $name, $provider, $processor, $stateOptions, $extraProperties);
104105
$this->itemUriTemplate = $itemUriTemplate;
105106
}
106107

src/Metadata/GraphQl/DeleteMutation.php

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public function __construct(
4545
?string $description = null,
4646
?array $normalizationContext = null,
4747
?array $denormalizationContext = null,
48+
?bool $collectDenormalizationErrors = null,
4849
?string $security = null,
4950
?string $securityMessage = null,
5051
?string $securityPostDenormalize = null,

src/Metadata/GraphQl/Operation.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public function __construct(
4949
?string $description = null,
5050
?array $normalizationContext = null,
5151
?array $denormalizationContext = null,
52+
?bool $collectDenormalizationErrors = null,
5253
?string $security = null,
5354
?string $securityMessage = null,
5455
?string $securityPostDenormalize = null,
@@ -75,7 +76,7 @@ public function __construct(
7576
?string $name = null,
7677
?string $provider = null,
7778
?string $processor = null,
78-
protected ?OptionsInterface $stateOptions = null,
79+
?OptionsInterface $stateOptions = null,
7980
array $extraProperties = [],
8081
) {
8182
// Abstract operation properties
@@ -96,6 +97,7 @@ public function __construct(
9697
$this->description = $description;
9798
$this->normalizationContext = $normalizationContext;
9899
$this->denormalizationContext = $denormalizationContext;
100+
$this->collectDenormalizationErrors = $collectDenormalizationErrors;
99101
$this->security = $security;
100102
$this->securityMessage = $securityMessage;
101103
$this->securityPostDenormalize = $securityPostDenormalize;
@@ -122,6 +124,7 @@ public function __construct(
122124
$this->name = $name;
123125
$this->provider = $provider;
124126
$this->processor = $processor;
127+
$this->stateOptions = $stateOptions;
125128
$this->extraProperties = $extraProperties;
126129
}
127130

src/Metadata/GraphQl/Query.php

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public function __construct(
4444
?string $description = null,
4545
?array $normalizationContext = null,
4646
?array $denormalizationContext = null,
47+
?bool $collectDenormalizationErrors = null,
4748
?string $security = null,
4849
?string $securityMessage = null,
4950
?string $securityPostDenormalize = null,

src/Metadata/GraphQl/QueryCollection.php

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public function __construct(
4545
?string $description = null,
4646
?array $normalizationContext = null,
4747
?array $denormalizationContext = null,
48+
?bool $collectDenormalizationErrors = null,
4849
?string $security = null,
4950
?string $securityMessage = null,
5051
?string $securityPostDenormalize = null,

src/Metadata/GraphQl/Subscription.php

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public function __construct(
4444
?string $description = null,
4545
?array $normalizationContext = null,
4646
?array $denormalizationContext = null,
47+
?bool $collectDenormalizationErrors = null,
4748
?string $security = null,
4849
?string $securityMessage = null,
4950
?string $securityPostDenormalize = null,

src/Metadata/HttpOperation.php

+2
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public function __construct(
121121
?string $description = null,
122122
?array $normalizationContext = null,
123123
?array $denormalizationContext = null,
124+
?bool $collectDenormalizationErrors = null,
124125
?string $security = null,
125126
?string $securityMessage = null,
126127
?string $securityPostDenormalize = null,
@@ -157,6 +158,7 @@ public function __construct(
157158
$this->deprecationReason = $deprecationReason;
158159
$this->normalizationContext = $normalizationContext;
159160
$this->denormalizationContext = $denormalizationContext;
161+
$this->collectDenormalizationErrors = $collectDenormalizationErrors;
160162
$this->validationContext = $validationContext;
161163
$this->filters = $filters;
162164
$this->elasticsearch = $elasticsearch;

src/Metadata/NotExposed.php

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public function __construct(
7676
?string $description = null,
7777
?array $normalizationContext = null,
7878
?array $denormalizationContext = null,
79+
?bool $collectDenormalizationErrors = null,
7980
?string $security = null,
8081
?string $securityMessage = null,
8182
?string $securityPostDenormalize = null,

src/Metadata/Operation.php

+14
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public function __construct(
6060
protected ?string $description = null,
6161
protected ?array $normalizationContext = null,
6262
protected ?array $denormalizationContext = null,
63+
protected ?bool $collectDenormalizationErrors = null,
6364
protected ?string $security = null,
6465
protected ?string $securityMessage = null,
6566
protected ?string $securityPostDenormalize = null,
@@ -327,6 +328,19 @@ public function withDenormalizationContext(array $denormalizationContext = []):
327328
return $self;
328329
}
329330

331+
public function getCollectDenormalizationErrors(): ?bool
332+
{
333+
return $this->collectDenormalizationErrors;
334+
}
335+
336+
public function withCollectDenormalizationErrors(bool $collectDenormalizationErrors = null): self
337+
{
338+
$self = clone $this;
339+
$self->collectDenormalizationErrors = $collectDenormalizationErrors;
340+
341+
return $self;
342+
}
343+
330344
public function getSecurity(): ?string
331345
{
332346
return $this->security;

src/Metadata/Patch.php

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public function __construct(
6969
?string $description = null,
7070
?array $normalizationContext = null,
7171
?array $denormalizationContext = null,
72+
?bool $collectDenormalizationErrors = null,
7273
?string $security = null,
7374
?string $securityMessage = null,
7475
?string $securityPostDenormalize = null,

src/Metadata/Post.php

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public function __construct(
7171
?string $description = null,
7272
?array $normalizationContext = null,
7373
?array $denormalizationContext = null,
74+
?bool $collectDenormalizationErrors = null,
7475
?string $security = null,
7576
?string $securityMessage = null,
7677
?string $securityPostDenormalize = null,

src/Metadata/Put.php

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public function __construct(
6969
?string $description = null,
7070
?array $normalizationContext = null,
7171
?array $denormalizationContext = null,
72+
?bool $collectDenormalizationErrors = null,
7273
?string $security = null,
7374
?string $securityMessage = null,
7475
?string $securityPostDenormalize = null,

src/RamseyUuid/Serializer/UuidDenormalizer.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
2929
try {
3030
return Uuid::fromString($data);
3131
} catch (InvalidUuidStringException $e) {
32-
throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
32+
throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, ['uuid'], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
3333
}
3434
}
3535

src/Serializer/AbstractConstraintViolationListNormalizer.php

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ protected function getMessagesAndViolations(ConstraintViolationListInterface $co
6262
'code' => $violation->getCode(),
6363
];
6464

65+
if ($hint = $violation->getParameters()['hint'] ?? false) {
66+
$violationData['hint'] = $hint;
67+
}
68+
6569
$constraint = $violation instanceof ConstraintViolation ? $violation->getConstraint() : null;
6670
if (
6771
[] !== $this->serializePayloadFields &&

0 commit comments

Comments
 (0)