Skip to content

[Normalizer] Breaking change in PR #61571 : Cannot denormalize magic properties #63270

@alexandre-le-borgne

Description

@alexandre-le-borgne

Symfony version(s) affected

7.4.5

Description

See this discussion for more details:
#63171

We cannot denormalize with magic __set method when properties do not exist
The code worked correctly until symfony/serializer 7.3.3

The bug that probably introduced in this PR #61571

Code:

abstract class TopicDto
{
    private string $topicName;
    private string|null $topicMessageKey;
    private int $topicMessageOffset;

    final public function setTopicInfo(string $name, string|null $key, int $offset): void
    {
        $this->topicName = $name;
        $this->topicMessageKey = $key;
        $this->topicMessageOffset = $offset;
    }

    public function getTopicName(): string
    {
        return $this->topicName;
    }

    public function getTopicMessageKey(): string|null
    {
        return $this->topicMessageKey;
    }

    public function getTopicMessageOffset(): int
    {
        return $this->topicMessageOffset;
    }
}
class GenericDto extends TopicDto
{
    public array $params = [];

    public function __set(string $name, mixed $value): void
    {
        $this->params[$name] = $value;
    }

    public function __get(string $name)
    {
        return $this->params[$name] ?? null;
    }

    public function __isset(string $name): bool
    {
        return true;
    }

    public function getData(): array
    {
        return array_merge(
            [
                'topic' => $this->getTopicName(),
                'topicMessageKey' => $this->getTopicMessageKey(),
                'topicMessageOffset' => $this->getTopicMessageOffset(),
            ],
            $this->params,
        );
    }
}
  $metadataFactory = new ClassMetadataFactory(new AttributeLoader());
     $serializer = new Serializer(
         [
             new ArrayDenormalizer(),
             new DateTimeNormalizer(
                 [
                     DateTimeNormalizer::FORMAT_KEY => 'Y-m-d\TH:i:s',
                     DateTimeNormalizer::TIMEZONE_KEY => 'Europe/Zurich', // date_default_timezone_get(),
                 ]
             ),
             new ObjectNormalizer(
                 $metadataFactory,
                 new MetadataAwareNameConverter($metadataFactory),
                 PropertyAccess::createPropertyAccessor(), //PropertyAccess::createPropertyAccessorBuilder()->enableMagicSet()->getPropertyAccessor(),
                 new PropertyInfoExtractor(
                     [
                         new ReflectionExtractor(),
                     ],
                     [
                         new PhpDocExtractor(),
                         new ReflectionExtractor(),
                     ],
                 ),
                 new ClassDiscriminatorFromClassMetadata($metadataFactory),
                 defaultContext: [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true]
             ),
         ],
     );

     $payload = ['param1' => 'test'];
     $dto = $serializer->denormalize($payload, GenericDto::class, context: [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true]);
     dd($dto);

Result:

Image

Expected:

Image

How to reproduce

https://github.com/alexandre-le-borgne/bug-symfony-denormalize-magic-properties

symfony serve
browse the index

Possible Solution

In Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php

Replace

return self::$isWritableCache[$class.$attribute] ??= str_contains($attribute, '.')
            || $this->propertyInfoExtractor->isWritable($class, $attribute)
            || !\in_array($this->writeInfoExtractor->getWriteInfo($class, $attribute)?->getType(), [null, PropertyWriteInfo::TYPE_NONE, PropertyWriteInfo::TYPE_PROPERTY], true);

by

            return self::$isWritableCache[$class.$attribute] ??= str_contains($attribute, '.')
            || $this->propertyInfoExtractor->isWritable($class, $attribute)
            || !\in_array($this->writeInfoExtractor->getWriteInfo($class, $attribute)?->getType(), [null, PropertyWriteInfo::TYPE_NONE], true);

Additional Context

No response

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