Skip to content

Commit 988304e

Browse files
committed
#[MapRequestPayload][Serializer] improve nested payload validation for MapRequestPayload using a new serialization context
1 parent aeb2489 commit 988304e

File tree

3 files changed

+105
-8
lines changed

3 files changed

+105
-8
lines changed

src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
2222
use Symfony\Component\HttpKernel\Exception\HttpException;
2323
use Symfony\Component\HttpKernel\HttpKernelInterface;
24-
use Symfony\Component\PropertyAccess\Exception\InvalidTypeException;
2524
use Symfony\Component\Serializer\Encoder\JsonEncoder;
2625
use Symfony\Component\Serializer\Encoder\XmlEncoder;
2726
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
@@ -285,6 +284,69 @@ public function testValidationNotPerformedWhenPartialDenormalizationReturnsViola
285284
}
286285
}
287286

287+
public function testNestedPayloadErrorReportingWhenPartialDenormalizationReturnsViolation()
288+
{
289+
$content = '{
290+
"name": "john doe",
291+
"address": {
292+
"address": "2332 street",
293+
"zipcode": "20220",
294+
"city": "Paris",
295+
"country": "75000",
296+
"geolocalization": {
297+
"lng": 32.423
298+
}
299+
}
300+
}';
301+
$serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]);
302+
303+
$validator = $this->createMock(ValidatorInterface::class);
304+
$validator->expects($this->never())
305+
->method('validate');
306+
307+
$resolver = new RequestPayloadValueResolver($serializer, $validator);
308+
$request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content);
309+
$kernel = $this->createMock(HttpKernelInterface::class);
310+
311+
// Test using use_class_as_default_expected_type = false context
312+
$argument = new ArgumentMetadata('invalid-nested-payload', Employee::class, false, false, null, false, [
313+
MapRequestPayload::class => new MapRequestPayload(serializationContext: ['use_class_as_default_expected_type' => false]),
314+
]);
315+
$arguments = $resolver->resolve($request, $argument);
316+
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
317+
318+
try {
319+
$resolver->onKernelControllerArguments($event);
320+
$this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
321+
} catch (HttpException $e) {
322+
$validationFailedException = $e->getPrevious();
323+
$this->assertInstanceOf(ValidationFailedException::class, $validationFailedException);
324+
$this->assertSame(
325+
sprintf('This value should be of type %s.', 'unknown'),
326+
$validationFailedException->getViolations()[0]->getMessage()
327+
);
328+
}
329+
330+
// Test using use_class_as_default_expected_type context
331+
$argument = new ArgumentMetadata('invalid-nested-payload', Employee::class, false, false, null, false, [
332+
MapRequestPayload::class => new MapRequestPayload(serializationContext: ['use_class_as_default_expected_type' => true]),
333+
]);
334+
$arguments = $resolver->resolve($request, $argument);
335+
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
336+
337+
try {
338+
$resolver->onKernelControllerArguments($event);
339+
$this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
340+
} catch (HttpException $e) {
341+
$validationFailedException = $e->getPrevious();
342+
$this->assertInstanceOf(ValidationFailedException::class, $validationFailedException);
343+
$this->assertSame(
344+
sprintf('This value should be of type %s.', Geolocalization::class),
345+
$validationFailedException->getViolations()[0]->getMessage()
346+
);
347+
}
348+
}
349+
288350
public function testUnsupportedMedia()
289351
{
290352
$serializer = new Serializer();
@@ -731,3 +793,34 @@ public function getPassword(): string
731793
return $this->password;
732794
}
733795
}
796+
797+
class Employee
798+
{
799+
public function __construct(
800+
public string $name,
801+
#[Assert\Valid]
802+
public ?Address $address = null,
803+
) {
804+
}
805+
}
806+
807+
class Address
808+
{
809+
public function __construct(
810+
public string $address,
811+
public string $zipcode,
812+
public string $city,
813+
public string $country,
814+
public Geolocalization $geolocalization,
815+
) {
816+
}
817+
}
818+
819+
class Geolocalization
820+
{
821+
public function __construct(
822+
public string $lat,
823+
public string $lng,
824+
) {
825+
}
826+
}

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
CHANGELOG
22
=========
33

4+
7.1
5+
---
6+
* Add `AbstractNormalizer::USE_CLASS_AS_DEFAULT_EXPECTED_TYPE` in order to use the FQCN as the default value for NotNormalizableValueException's expectedTypes instead of unknown
7+
48
7.0
59
---
610

src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
118118
*/
119119
public const REQUIRE_ALL_PROPERTIES = 'require_all_properties';
120120

121+
/**
122+
* Use class name as default expected type when throwing NotNormalizableValueException instead of unknown.
123+
*/
124+
public const USE_CLASS_AS_DEFAULT_EXPECTED_TYPE = 'use_class_as_default_expected_type';
125+
121126
/**
122127
* @internal
123128
*/
@@ -380,7 +385,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex
380385
$exception = NotNormalizableValueException::createForUnexpectedDataType(
381386
sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name),
382387
$data,
383-
['unknown'],
388+
[isset($context[self::USE_CLASS_AS_DEFAULT_EXPECTED_TYPE]) && $context[self::USE_CLASS_AS_DEFAULT_EXPECTED_TYPE] ? $class : 'unknown'],
384389
$context['deserialization_path'] ?? null,
385390
true
386391
);
@@ -424,12 +429,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex
424429
unset($context['has_constructor']);
425430

426431
if (!$reflectionClass->isInstantiable()) {
427-
throw NotNormalizableValueException::createForUnexpectedDataType(
428-
sprintf('Failed to create object because the class "%s" is not instantiable.', $class),
429-
$data,
430-
['unknown'],
431-
$context['deserialization_path'] ?? null
432-
);
432+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Failed to create object because the class "%s" is not instantiable.', $class), $data, ['unknown'], $context['deserialization_path'] ?? null);
433433
}
434434

435435
return new $class();

0 commit comments

Comments
 (0)
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