Description
Symfony version(s) affected
5.4 and above
Description
In denormalizing to object by constructor, symfony serializer is not sending deserialization_path
in $context.
In AbstractObjectNormalizer there is foreach which goes through all attributes and creating context (symfony/serializer/Normalizer/AbstractObjectNormalizer.php:374)
foreach ($normalizedData as $attribute => $value) {
$attributeContext = $this->getAttributeDenormalizationContext($resolvedClass, $attribute, $context);
private function getAttributeDenormalizationContext(string $class, string $attribute, array $context): array
{
// (...)
$context['deserialization_path'] = ($context['deserialization_path'] ?? false) ? $context['deserialization_path'].'.'.$attribute : $attribute;
// (...)
}
And it works fine when we want to denormalize by properties etc.
Problem occurs if object has only constructor available, then Serializer fires AbstractNormalizer.
In AbstractNormalizer there is foreach which goes through construct arguments
(symfony/serializer/Normalizer/AbstractNormalizer.php:362)
foreach ($constructorParameters as $constructorParameter) {
$paramName = $constructorParameter->name;
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
key
would be equivalent to attribute
from AbstractObjectNormalizer, hence the question why not add key
to context as deserialization_path
as construct arguments needs to be same as class attributes.
When you want to use Collecting Type Errors While Denormalizing path is empty, so you cannot return info which property had incorrect type.
How to reproduce
To reproduce you will need serializer, annotations and property access
composer require symfony/serializer
composer require doctrine/annotations
composer require symfony/property-access
example.php:
<?php
// composer require symfony/serializer
// composer require doctrine/annotations
// composer require symfony/property-access
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
require 'vendor/autoload.php';
class ExampleObject
{
private int $number;
public function __construct(int $number)
{
$this->number = $number;
}
}
class ExampleDTO
{
private ExampleObject $exampleObject;
public function __construct(ExampleObject $exampleObject)
{
$this->exampleObject = $exampleObject;
}
}
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$serializer = new Serializer([new ObjectNormalizer($classMetadataFactory)],['json' => new JsonEncoder()]);
try {
$data = $serializer->deserialize(
'{"exampleObject":"test"}',
ExampleDTO::class,
'json',
[
DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true
]
);
print_r($data);
} catch (\Symfony\Component\Serializer\Exception\PartialDenormalizationException $e) {
// as seen at https://symfony.com/doc/5.4/components/serializer.html#collecting-type-errors-while-denormalizing
/** @var NotNormalizableValueException */
foreach ($e->getErrors() as $exception) {
echo sprintf('The type must be one of "%s" ("%s" given). in: "%s"',
implode(', ', $exception->getExpectedTypes()),
$exception->getCurrentType(),
$exception->getPath()
) . PHP_EOL;
// Outputs: The type must be one of "unknown" ("array" given). in: "" <- no path and expected type is unknown
}
}
Above example will output The type must be one of "unknown" ("array" given). in: "" <- no path and expected type is unknown
Possible Solution
For the path in \Symfony\Component\Serializer\Normalizer\AbstractNormalizer::instantiateObject
add at the beginning of constructor arguments foreach or same context build as in AbstractObjectNormalizer:
$context['deserialization_path'] = $key;
As for the "unknown" I have not looked into it.
Additional Context
I use DTOs in my project which have custom denormalizers which shows same issue.
Also I have noticed issue when collecting errors that despite mismatch of constructor arguments Serializer Component will try to create object resulting in \Symfony\Component\Serializer\Exception\ExceptionInterface
when using custom Denormalizer which throws NotNormalizableValueException
in denormalize
method and I think it is a bit odd.