Closed
Description
Symfony version(s) affected
6.1, 6.0, 5.4
Description
The way nullable objects in maps (e.g. array<string, Dummy|null>
) are denormalized is inconsistent to the behaviour when denormalizing the same value type (in this case Dummy|null
) to a plain attribute.
Problem 1) Object values of a map can never be null
- denormalizing
['foo' => ['bar' => null]]
to@var array<string,DummyValue|null> $foo
will always denormalize to an empty instance ofDummyValue
:['foo' => ['bar' => object(DummyValue)]]
(expected['foo' => ['bar' => null]]
- you will always end up in a
output != input
state when doing some thing likeinput:json => deserialize() => serialize() => output:json
- though it works (
['foo' => null]
) when it's a plain (non-map) attribute/property value (@var DummyValue|null $foo
)
Problem 2) It's impossible to use discriminated/abstract types in maps where the value is nullable
- denormalizing
['foo' => ['bar' => null]]
to@var array<string,AbstractDummyValue|null> $foo
(whereAbstractDummyValue
has a discriminator mapping) it will throwNotNormalizableValueException
complaining the discriminator field is not set. - also works as expected (
['foo' => null]
) if it's a plain (non-map) attribute/property value (@var AbstractDummyValue|null $foo
)
How to reproduce
$loaderMock = new class() implements ClassMetadataFactoryInterface {
public function getMetadataFor($value): ClassMetadataInterface
{
if (AbstractDummyValue::class === $value) {
return new ClassMetadata(
AbstractDummy::class,
new ClassDiscriminatorMapping('type', [
'dummy' => DummyValue::class,
'another-dummy' => AnotherDummyValue::class,
])
);
}
throw new InvalidArgumentException();
}
public function hasMetadataFor($value): bool
{
return AbstractDummyValue::class === $value;
}
};
$factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new ObjectNormalizer($factory, null,null, new PhpDocExtractor(), new ClassDiscriminatorFromClassMetadata($loaderMock));
$serializer = new Serializer([$normalizer, new ArrayDenormalizer()]);
$normalizer->setSerializer($serializer);
Steps to reproduce 1) ("Object values of a map can never be null
")
class DummyValue
{
}
class DummyMapOfStringToNullableObject {
/**
* @var array<string,DummyValue|null>
*/
public $map;
}
// Since `$map` is nullable Id expect `$map['assertNull']` to be `null`.
$normalizedData = $serializer->denormalize([
'map' => [
'assertDummyMapValue' => [
'value' => 'foo'
],
'assertNull' => null, // will be denormalized to an empty `DummyValue()`
],
], DummyMapOfStringToNullableObject::class);
... while it works actually for the following scenario:
class DummyNullableObjectValue
{
/**
* @var DummyValue|null $object
*/
public $object;
/**
* @var DummyValue|null $object
*/
public $nullObject;
}
$normalizedData = $serializer->denormalize([
'object' => [
'value' => 'foo',
],
'nullObject' => null, // will be denormalized as expected to `null`
], DummyNullableObjectValue::class);
see full tests here:
Steps to reproduce 2) ("It's impossible to use discriminated/abstract types in maps where the value is nullable")
abstract class AbstractDummyValue
{
public $value;
}
class DummyValue extends AbstractDummyValue
{
}
class AnotherDummyValue extends AbstractDummyValue
{
}
class DummyMapOfStringToNullableAbstractObject {
/**
* @var array<string,AbstractDummyValue|null>
*/
public $map;
}
// FIXME throws `NotNormalizableValueException: Type property "type" not found for the abstract object "Symfony\Component\Serializer\Tests\Normalizer\AbstractDummyValue".`
$normalizedData = $serializer->denormalize([
'map' => [
'assertNull' => null,
],
], DummyMapOfStringToNullableAbstractObject::class);
see full tests here:
Possible Solution
Additional Context
No response