Closed
Description
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