Open
Description
Symfony version(s) affected
7.3.1
Description
Hello,
When deserializing a json document with properties written in snake_case, the behaviour seems inconsistent depending on the property visibility and the use of getters/setters or hooks.
- public properties are not deserialized
- private properties with getters / setters are deserialized
- private properties with hook are not deserialized
How to reproduce
Given this class which is the deserialization target:
namespace App\Model;
class TestDeserialize {
public string $publicKey = "";
private string $privateKey = "";
private string $hookedKey = "" {
get => $this->hookedKey;
set => $this->hookedKey = $value;
}
public function getPrivateKey(): string
{
return $this->privateKey;
}
public function setPrivateKey(string $privateKey): void
{
$this->privateKey = $privateKey;
}
}
Below is a command which reproduce the issue:
<?php
namespace App\Command;
use App\Model\TestDeserialize;
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\Serializer\SerializerInterface;
#[AsCommand(
name: 'test:deserialize',
description: 'Add a short description for your command',
)]
class TestSerialCommand extends Command
{
private const string JSON =
<<<JSON
{
"public_key": "foo",
"private_key": "bar",
"hooked_key": "fuz"
}
JSON;
public function __construct(private readonly SerializerInterface $serializer)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
dump($this->serializer->deserialize(self::JSON, TestDeserialize::class, 'json'));
return Command::SUCCESS;
}
}
In the dump, we can see the inconsistency of the result:
App\Model\TestDeserialize^ {#1016
+publicKey: ""
-privateKey: "bar"
-hookedKey: ""
}
Possible Solution
No response
Additional Context
I have been able to track down the issue to \Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor::isWritable()
(which is called by \Symfony\Component\Serializer\Normalizer\ObjectNormalizer::isAllowedAttribute()
). In the following lines, a reflectionMethod will be found for a private property with a setter method, whereas it will return null in other cases.
// First test with the camelized property name
[$reflectionMethod] = $this->getMutatorMethod($class, $this->camelize($property));
if (null !== $reflectionMethod) {
return true;
}