Skip to content

[Serializer] deserialization_path missing in custom DenormalizerInterface upon using object's constructor #44925

Closed
@szymat

Description

@szymat

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions