Skip to content

[serializer] Code regression by adding type hints #44872

@JustDylan23

Description

@JustDylan23

Symfony version(s) affected

^6.0

Description

A TypeError is thrown by the Symfony serializer when trying to denormalize a string value into object.

class Author {
    public $name;
}

class Blog {
    public $title;
    public Author $author;
}

// when there is no issue
$blog = $denormalizer->denormalize([
    'title' => 'test title',
    'author' => [
        'name' => 'test name',
    ],
], Blog::class);


// when a type error is thrown
$blog = $denormalizer->denormalize([
    'title' => 'test title',
    'author' => 'test string',
], Blog::class);

The expected behavior is the deserializer throwing a Symfony\Component\Serializer\Exception\UnexpectedValueException exception.
When recieving JSON through an API this can be quite annoying if you want to tell the user that they sent malformed json. You'd currently have to catch the ValueException which is not ideal.

How to reproduce

The issue can be reproducted by cloning this repository:

TypeError:
Symfony\Component\Serializer\Normalizer\AbstractNormalizer::prepareForDenormalization(): Argument #1 ($data) must be of type object|array|null, string given, called in /var/www/symfony/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php on line 368

  at vendor/symfony/serializer/Normalizer/AbstractNormalizer.php:299
  at Symfony\Component\Serializer\Normalizer\AbstractNormalizer->prepareForDenormalization('test string')
     (vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php:368)
  at Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->denormalize('test string', 'App\\Entity\\User', null, array('cache_key' => 'c93a6d4efa206ea58a62cc6b7fab8dfb', 'deserialization_path' => 'author'))
     (vendor/symfony/serializer/Serializer.php:238)
  at Symfony\Component\Serializer\Serializer->denormalize('test string', 'App\\Entity\\User', null, array('cache_key' => 'c93a6d4efa206ea58a62cc6b7fab8dfb', 'deserialization_path' => 'author'))
     (vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php:559)
  at Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->validateAndDenormalize(array(object(Type)), 'App\\Entity\\Blog', 'author', 'test string', null, array('cache_key' => '44db5a926a1544b1a8585af40107ca3a', 'deserialization_path' => 'author'))
     (vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php:401)
  at Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->denormalize(array('title' => 'test title', 'author' => 'test string'), 'App\\Entity\\Blog', null, array('cache_key' => '44db5a926a1544b1a8585af40107ca3a'))
     (vendor/symfony/serializer/Serializer.php:238)
  at Symfony\Component\Serializer\Serializer->denormalize(array('title' => 'test title', 'author' => 'test string'), 'App\\Entity\\Blog')
     (src/Controller/BugReproductionController.php:19)
  at App\Controller\BugReproductionController->test(object(Serializer))
     (vendor/symfony/http-kernel/HttpKernel.php:152)
  at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
     (vendor/symfony/http-kernel/HttpKernel.php:74)
  at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
     (vendor/symfony/http-kernel/Kernel.php:202)
  at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
     (vendor/symfony/runtime/Runner/Symfony/HttpKernelRunner.php:35)
  at Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner->run()
     (vendor/autoload_runtime.php:29)
  at require_once('/var/www/symfony/vendor/autoload_runtime.php')
     (public/index.php:5)         

Possible Solution

I think a passable solution would be to add type validation in the Symfony\Component\Serializer\Normalizer\AbstractNormalizer::prepareForDenormalization function

To give some extra context, the function is called here:

public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
{
if (!isset($context['cache_key'])) {
$context['cache_key'] = $this->getCacheKey($format, $context);
}
$allowedAttributes = $this->getAllowedAttributes($type, $context, true);
$normalizedData = $this->prepareForDenormalization($data);
$extraAttributes = [];
$reflectionClass = new \ReflectionClass($type);
$object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format);
$resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
foreach ($normalizedData as $attribute => $value) {
$attributeContext = $this->getAttributeDenormalizationContext($resolvedClass, $attribute, $context);

Without the fix:

/**
 * Normalizes the given data to an array. It's particularly useful during
 * the denormalization process.
 */
protected function prepareForDenormalization(object|array|null $data): array
{
    return (array) $data;
}

With the fix

/**
 * Normalizes the given data to an array. It's particularly useful during
 * the denormalization process.
 */
protected function prepareForDenormalization(mixed $data): array
{
    if (is_scalar($data)) {
        throw new Symfony\Component\Serializer\Exception\UnexpectedValueException();
    }

    return (array) $data;
}

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