Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Serializer] inconsistent behaviour of nullable objects in key/value arrays #45883

Copy link
Copy link
Closed
@phramz

Description

@phramz
Issue body actions

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 of DummyValue: ['foo' => ['bar' => object(DummyValue)]] (expected ['foo' => ['bar' => null]]
  • you will always end up in a output != input state when doing some thing like input: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 (where AbstractDummyValue has a discriminator mapping) it will throw NotNormalizableValueException 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

#45884

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

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.