Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Serializer] Discriminator is removed when #[Ignore] attribute used on unrelated method #60214

Closed
@ruudk

Description

@ruudk

Symfony version(s) affected

7.2.5

Description

Today I noticed something weird with the Serializer. I have a LoaderInterface that defines a discriminator mapping dynamically. This works great.

As soon as I start using the #[Ignore] attribute in an unrelated method, the discriminator is no longer added to the serialized result.

I created a small isolated reproducer that explains the problem.

How to reproduce

<?php

declare(strict_types=1);

use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Attribute\Ignore;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer;
use Symfony\Component\Serializer\Serializer;

include 'vendor/autoload.php';

final class DiscriminatorLoader implements LoaderInterface
{
    #[Override]
    public function loadClassMetadata(ClassMetadataInterface $classMetadata) : bool
    {
        if (!in_array($classMetadata->getName(), [ParentInterface::class, ParentWithIgnoreAttributeInterface::class], true)) {
            return false;
        }

        $classMetadata->setClassDiscriminatorMapping(
            new ClassDiscriminatorMapping(
                'discriminator',
                ['Foo1' => Foo1::class, 'Foo2' => Foo2::class],
            ),
        );

        return true;
    }
}

$loader = new LoaderChain([
    new DiscriminatorLoader(),
    new AttributeLoader(),
]);

$classMetadataFactory = new ClassMetadataFactory($loader);
$propertyAccessor = PropertyAccess::createPropertyAccessor();
$reflectionExtractor = new ReflectionExtractor();
$nameConverter = new MetadataAwareNameConverter($classMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
$propertyTypeExtractor = new PropertyInfoExtractor(
    listExtractors: [$reflectionExtractor],
    typeExtractors: [new PhpStanExtractor(), $reflectionExtractor],
    accessExtractors: [$reflectionExtractor],
    initializableExtractors: [$reflectionExtractor],
);
$classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);

$serializer = new Serializer(
    [
        new UnwrappingDenormalizer($propertyAccessor),
        new DateTimeNormalizer(),
        new BackedEnumNormalizer(),
        new PropertyNormalizer(
            $classMetadataFactory,
            $nameConverter,
            $propertyTypeExtractor,
            $classDiscriminatorResolver,
            null,
            [],
        ),
        new ArrayDenormalizer(),
        new ObjectNormalizer(
            $classMetadataFactory,
            $nameConverter,
            $propertyAccessor,
            $propertyTypeExtractor,
            $classDiscriminatorResolver,
        ),
    ],
    [new JsonEncoder()],
);

interface ParentWithIgnoreAttributeInterface
{
    #[Ignore]
    public function getDisplayName() : string;
}

final class Foo1 implements ParentWithIgnoreAttributeInterface
{
    #[Override]
    public function getDisplayName() : string
    {
        return 'Some title';
    }

    public function __construct(
        public int $id,
    ) {
    }
}

// When `#[Ignore]` is used on the `getDisplayName` method in ParentWithIgnoreAttributeInterface, it does not show the discriminator:
// result: {"context":{"id":1}}
dump($serializer->serialize(new Foo1(1), 'json'));

interface ParentInterface
{
    // #[Ignore] commented out
    public function getDisplayName() : string;
}

final class Foo2 implements ParentInterface
{
    #[Override]
    public function getDisplayName() : string
    {
        return 'Some title';
    }

    public function __construct(
        public int $id,
    ) {
    }
}

// When `#[Ignore]` is removed from `getDisplayName` method in ParentInterface, it does show the discriminator:
// result: "{"context":{"discriminator":"Foo2","id":1}}
dump($serializer->serialize(new Foo2(1), 'json'));

Possible Solution

I have the feeling this is related to the PropertyNormalizer. When I remove that from the Serializer it works. But since this is a default normalizer, I'd like to keep this.

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