I’m trying to deserialize a nested JSON object to Data Transfer Objects with the Symfony Serializer in a legacy Symfony application, upgraded recently to 5.4.
As soon as my JSON contains another object, the deserialization fails, when using the Symfony Serializer as DI with an error message like this:
App\Command\Material::setAmount(): Argument #1 ($amount) must be of type App\Command\Amount, array given, called in …./v
endor/symfony/property-access/PropertyAccessor.php on line 591
Doing the same in a fresh symfony 5.4 installation, the deserialization looks pretty fine:
<?php
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
#[AsCommand(
name: 'app:test',
description: 'Add a short description for your command',
)]
class TestCommand extends Command
{
public function __construct(private SerializerInterface $serializer)
{
parent::__construct();
}
protected function configure(): void
{
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$amount = new Amount();
$amount->setValue('1,99');
$amount->setCurrency('EUR');
$material = Material::creatFromData(1, 'code1', 'name1', $amount, 1, true, true, true);
$encoders = [new JsonEncoder()];
$normalizers = [new ObjectNormalizer()];
$serializer = new Serializer($normalizers, $encoders);
$json = $serializer->serialize($material, 'json');
$output->writeln($json);
$deserialize = $serializer->deserialize($json, Material::class, 'json');
var_dump($deserialize);
$output->writeln($json);
return Command::SUCCESS;
}
}
Output:
{"id":1,"code":"code1","name":"name1","flag":1,"needsCollectData":true,"noteAllowed":true,"deprecated":true,"amount":{"value":"1,99","currency":"EUR"}}
object(App\Command\Material)#193 (6) {
["id":"App\Command\Material":private]=>
int(1)
["code":"App\Command\Material":private]=>
string(5) "code1"
["name":"App\Command\Material":private]=>
string(5) "name1"
["amount":"App\Command\Material":private]=>
object(App\Command\Amount)#224 (2) {
["value":"App\Command\Amount":private]=>
string(4) "1,99"
["currency":"App\Command\Amount":private]=>
string(3) "EUR"
}
["flag":"App\Command\Material":private]=>
int(1)
["needsCollectData":"App\Command\Material":private]=>
bool(true)
["isNoteAllowed":"App\Command\Material":private]=>
uninitialized(bool)
["isDeprecated":"App\Command\Material":private]=>
uninitialized(bool)
}
You can see, that the nested object $amount is deserialized properly.
To be able to deserialize nested objects in my legacy application, I need to overwrite the SerializerInterface and use my own service like this:
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
/**
* A custom Serializer is needed because using the Symfony serializer through DI does not deserialize object properties because \Symfony\Component\Serializer\Normalizer\ObjectNormalizer::__construct(propertyTypeExtractor) is null for some reason.
*/
class Serializer implements SerializerInterface
{
private SerializerInterface $serializer;
public function __construct()
{
$encoder = [new JsonEncoder()];
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
$normalizer = [new ArrayDenormalizer(), new ObjectNormalizer(null, null, null, $extractor)];
$this->serializer = new \Symfony\Component\Serializer\Serializer($normalizer, $encoder);
}
public function serialize($data, string $format, array $context = [])
{
return $this->serializer->serialize($data, $format, $context);
}
public function deserialize($data, string $type, string $format, array $context = [])
{
return $this->serializer->deserialize($data, $type, $format, $context);
}
}
My investigations tell me that the ObjectNormalizer is missing somehow. How can I pass the Normalizer to the serializer loaded via DI?
I can’t find anything in the documentation about how to pass normalizer options to a deserialization context (4th parameter) or add anything relevant to the config.yml.
I’d be glad to avoid overwriting the default Serializer but I couldn’t find any valid solution to move forward.
Thank you in advance.