Tuesday, 3 November 2020

Deserialize JSON into an array of class instances using Symfony Serializer

I'm trying to deserialize JSON into a DTO class, which has a property types as NestedDto[]. Unfortunately, the objects inside the array are deserialized as arrays, not instances of NestedDto. My DTO classes are like:

class TestDto
{
    /**
    * @var NestedDto
    */
    public NestedDto $c;

    /**
     * @var NestedDto[] $cs
     */
    public array $cs;
}

class NestedDto
{
    public string $s;
}

The code that orchestrates this use-case (I will show the code for the serializer at the end of the question):

function main() {
    $serializer = new JsonSerializer();
    $json = '{"c":{"s":"nested"},"cs":[{"s":"nested1"},{"s":"nested2"}]}';
    $dto = $serializer->fromJSON(TestDto::class, $json);
    echo '<pre>' . print_r($dto, true) . '</pre>';
    $response = $serializer->toJSON($dto);
    exit($response);
}

You can see that the property c is a proper NestedDto, but inside the cs array we have two arrays instead of two NestedDto instances.

TestDto Object
(
    [c] => NestedDto Object
        (
            [s] => nested
        )

    [cs] => Array
        (
            [0] => Array
                (
                    [s] => nested1
                )
            [1] => Array
                (
                    [s] => nested2
                )
        )
)

Here is the code for the JsonSerializer class which is a wrapper around the Symfony serializer:

final class JsonSerializer
{

    private Serializer $serializer;
    private ValidatorInterface $validator;

    /**
     * JsonSerializer constructor.
     */
    public function __construct()
    {
        $encoders = [new JsonEncoder()];

        $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
        $normalizers = [
            new ObjectNormalizer($classMetadataFactory, NULL, NULL, new ReflectionExtractor()),
            new ArrayDenormalizer(),
            new DateTimeNormalizer(),
        ];

        $this->serializer = new Serializer($normalizers, $encoders);
        $this->validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator();
    }

    /**
     * Create a DTO class from json. Will also auto-validate the DTO.
     * @param $classFQN string The class FQN, e.g. \App\Something\CreateQuoteDTO::class
     * @param $json string The string containing JSON we will use for the DTO.
     * @return array|mixed|object An instance of the $class.
     */
    public function fromJSON( string $classFQN, string $json )
    {
        $instance = $this->serializer->deserialize($json, $classFQN, 'json');
        $errors = $this->validator->validate($instance);
        if (count($errors) > 0) {
            throw new \RuntimeException('Invalid DTO.');
        }

        return $instance;
    }

    /**
     * Convert a class instance to JSON string
     * @param $object
     * @return string
     */
    public function toJSON( $object ) : string
    {
        return $this->serializer->serialize($object, 'json');
    }

}

I have read all of the similar questions on Stack Overflow, but I fail to find what is missing in my code. My guess is something isn't wired up correctly in the normalizers used in JsonSerializer, but after spending an inordinate amount of time on this, I'm out of clues.

Here's all the code in a sandbox where you can run it: phpsandbox.io

Just in case, the packages I've required with composer for this example are:

"symfony/serializer-pack": "*",
"symfony/validator": "*",
"doctrine/annotations": "*",
"doctrine/cache": "*",



from Deserialize JSON into an array of class instances using Symfony Serializer

No comments:

Post a Comment