Skip to content

Allow AbstractNormalizer to use null for nullable constructor parameters without default value #40511

@pounard

Description

@pounard

Symfony version(s) affected: All versions until 5.1 (but I guess 5.2 is too)

Description

Serializer component AbstractNormalizer attemps to guess constructor parameters, and falls back using default values when possible, which is good. Yet, it misses one use case: when you specify nullable non-optional parameter, case in which null is a valid value, not the default one, yet still valid.

It's a matter a choice, either we consider that the incoming data should explicitly set null here, or we consider that it might come from a dynamic language such as JavaScript that may remove undefined values from sent JSON: case in which we might want to fix that and consider that undefined is null.

How to reproduce

Simply write a class with a constructor as such:

class Foo
{
    public function __construct(string $foo, ?string $bar)
    {
    }
}

And attempt to denormalize using this incoming array:

$input = [
    'foo' => 'any value',
];

In this specific case, null is an allowed value for $bar, and value is not set in $input, why not simply give it null ? Actual behaviour is that the exception for missing constructor argument is raised and prevents the object from being denormalized.

Possible Solution

Easy two-line fix, in AbstractNormalizer::instantiateObject() as such:

Replace:

                } elseif (\array_key_exists($key, $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
                    $params[] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
                    $params[] = $constructorParameter->getDefaultValue();
                } else {
                    throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
                }

Using:

                } elseif (\array_key_exists($key, $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
                    $params[] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
                    $params[] = $constructorParameter->getDefaultValue();
                } elseif ($constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) {
                    $params[] = null;
                } else {
                    throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
                }

I just added those two lines:

                } elseif ($constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) {
                    $params[] = null;

Compatible for all PHP version from 7.0 if I read correctly https://www.php.net/manual/en/reflectiontype.allowsnull.php

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