Denormalize Nested Structure in Objects with Symfony 2 Serializer

How do I deserialize a JSON object - which has a nested property - to a Symfony entity?

A custom normalizer does the trick. Code follows.

<?php

namespace App\Serializer\Denormalizer;

use App\Entity\Vehicle;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;

class VehicleDenormalizer implements CacheableSupportsMethodInterface, ContextAwareDenormalizerInterface
{
protected ObjectNormalizer $normalizer;
protected PropertyAccessor $propertyAccessor;

public function __construct(ObjectNormalizer $normalizer)
{
$this->normalizer = $normalizer;
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
}

public function denormalize($data, $type, $format = null, array $context = []): Vehicle
{
/** @var Vehicle */
$vehicle = $this->normalizer->denormalize($data, $type, $format, $context);

// It's possible to directly access the values, but that requires error
// checking. This method will return a null if it doesn't exist.
$colour = $this->propertyAccessor->getValue($data, '[meta][colour]');
$vehicle->setColour($colour);

return $vehicle;
}

public function supportsDenormalization($data, $type, $format = null, array $context = [])
{
return Vehicle::class == $type;
}

public function hasCacheableSupportsMethod(): bool
{
return true;
}
}

To use this denormalizer, you can either inject the SerializerInterface or explicitly create the serializer (code follows).

$json = '
{
"make": "VW Golf",
"meta": {
"colour": "red"
}
}
';
$carJson = json_encode($json);

// These 2 lines let us use the @SerializedName annotation
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
$objectNormalizer = new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter);

$normalizers = [new VehicleDenormalizer($objectNormalizer)];
$serializer = new Serializer($normalizers);

/** @var Vehicle */
$vehicle = $serializer->denormalize(
$carJson,
Vehicle::class,
);

$vehicle->getMake(); // VW Golf
$vehicle->getColour(); // red

I'm explicitly creating the serializer because for some reason when the source data was a string and the destination was an integer, the injection method does not automatically convert the types and I get the following error:

The type of the "VehicleNo" attribute for class "App\Entity\Vehicle" must be one of "int" ("string" given).

(This code example isn't using VehicleNo as I've simplified it, but included it here to show an example of an error message where, say, Vehicle has a property $vehicleNo of type int).

How to deserialize a nested array of objects declared on the constructor via promoted properties, with Symfony Serializer?

Apparently the issue is that the PhpDocExtractor does not extract properties from constructors. You need to use a specific extractor for this:

use Symfony\Component\PropertyInfo;
use Symfony\Component\Serializer;

$phpDocExtractor = new PropertyInfo\Extractor\PhpDocExtractor();
$typeExtractor = new PropertyInfo\PropertyInfoExtractor(
typeExtractors: [ new PropertyInfo\Extractor\ConstructorExtractor([$phpDocExtractor]), $phpDocExtractor,]
);

$serializer = new Serializer\Serializer(
normalizers: [
new Serializer\Normalizer\ObjectNormalizer(propertyTypeExtractor: $typeExtractor),
new Serializer\Normalizer\ArrayDenormalizer(),
],
encoders: ['json' => new Serializer\Encoder\JsonEncoder()]
);

With this you'll get the desired results. Took me a bit to figure it out. The multiple denormalizer/extractor chains always get me.


Alternatively, for more complex os specialized situations, you could create your own custom denormalizer:

use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait

class UserDenormalizer
implements DenormalizerInterface, DenormalizerAwareInterface
{

use DenormalizerAwareTrait;

public function denormalize($data, string $type, string $format = null, array $context = [])
{
$addressBook = array_map(fn($address) => $this->denormalizer->denormalize($address, AddressDTO::class), $data['addressBook']);

return new UserDTO(
name: $data['name'],
age: $data['age'],
billingAddress: $this->denormalizer->denormalize($data['billingAddress'], AddressDTO::class),
shippingAddress: $this->denormalizer->denormalize($data['shippingAddress'], AddressDTO::class),
addressBook: $addressBook
);
}

public function supportsDenormalization($data, string $type, string $format = null)
{
return $type === UserDTO::class;
}
}

Setup would become this:

$extractor = new PropertyInfoExtractor([], [
new PhpDocExtractor(),
new ReflectionExtractor(),

]);

$userDenormalizer = new UserDenormalizer();
$normalizers = [
$userDenormalizer,
new ObjectNormalizer(null, null, null, $extractor),
new ArrayDenormalizer(),

];
$serializer = new Serializer($normalizers, [new JsonEncoder()]);
$userDenormalizer->setDenormalizer($serializer);

Output becomes what you would expect:

^ UserDTO^ {#39
+name: "John"
+age: 25
+billingAddress: AddressDTO^ {#45
+street: "Rue Paradis"
+city: "Marseille"
}
+shippingAddress: null
+addressBook: array:2 [
0 => AddressDTO^ {#46
+street: "Rue Paradis"
+city: "Marseille"
}
]
}

How to use Symfony Serializer on a nested object with multiple instances of child?

By experimentation I found this works:

<?php
/**
* Symfony Serializer experiment
*/
require $_SERVER['DOCUMENT_ROOT'].'/libraries/symfony/vendor/autoload.php';

use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;

Class Number {
private $Number;
// Getters
public function getNumber()
{
return $this->Number;
}
// Setters
public function setNumber($number)
{
$this->Number = $number;

return $this;
}
}

class Contact
{
private $Name;
private $Numbers;

// Getters
public function getName()
{
return $this->Name;
}
public function getNumbers()
{
return $this->Numbers;
}
// Setters
public function setName($name)
{
$this->Name = $name;

return $this;
}
public function setNumbers($numbers)
{
$this->Numbers = $numbers;

return $this;
}
public function addNumber($number) {
$this->Numbers['Number'][] = $number;
}
}

$data = <<<EOF
<Contact>
<Name>foo</Name>
<Numbers>
<Number>02378415326</Number>
<Number>07865412354</Number>
</Numbers>
</Contact>
EOF;

// The default Root tag for the XmlEncode is <response>. Not what we want so let's change it to <Contact>
$xmlEncoder = new XmlEncoder();
$xmlEncoder->setRootNodeName('Contact');

$encoders = array($xmlEncoder, new JsonEncoder());

$normalizers = array(new ObjectNormalizer(), new ArrayDenormalizer());

$serializer = new Serializer($normalizers, $encoders);

//$jsonContent = $serializer->serialize($searchId, 'json');

$contact = $serializer->deserialize($data, Contact::class, 'xml');

$contact->addNumber('01234567890');

$xmlContent = $serializer->serialize($contact, 'xml');

echo "<pre lang=xml>";
echo htmlentities($xmlContent);
echo "</pre>";
?>

After running deserialization on the xml (xml->php), the Contact object I got was able to add a further Number.

Output:

<?xml version="1.0"?>
<Contact>
<Name>foo</Name>
<Numbers>
<Number>02378415326</Number>
<Number>07865412354</Number>
<Number>01234567890</Number>
</Numbers>
</Contact>

It works in both directions using no additional directives.

How to deserialize an array of complex objects with Symfony Serializer?

I was able to solve it with a custom PropertyExtractor that points the serializer to the correct type:

$encoders = [new XmlEncoder('response', LIBXML_NOERROR)];
$normalizers = [
new ArrayDenormalizer(),
new ObjectNormalizer(null, null, null,
new class implements PropertyTypeExtractorInterface
{
private $reflectionExtractor;

public function __construct()
{
$this->reflectionExtractor = new ReflectionExtractor();
}

public function getTypes($class, $property, array $context = array())
{
if (is_a($class, Outer::class, true) && 'Inner' === $property) {
return [
new Type(Type::BUILTIN_TYPE_OBJECT, true, Inner::class . "[]")
];
}
return $this->reflectionExtractor->getTypes($class, $property, $context);
}
})
];
$this->serializer = new Serializer($normalizers, $encoders);

Symfony Serializer: Denormalize (Deserialize) awkward array data

Here is the solution I ended up with, I am not completely convinced this is the best way, but its working for the moment. Any feedback welcome:

class StrangeDataDenormalizer extends Symfony\Component\Serializer\Normalizer\ObjectNormalizer
{
protected function isStrangeDataInterface(string $type): bool
{
try {
$reflection = new \ReflectionClass($type);

return $reflection->implementsInterface(StrangeDataInterface::class);
} catch (\ReflectionException $e) { // $type is not always a valid class name, might have extract junk like `[]`
return false;
}
}

public function denormalize($data, $class, $format = null, array $context = array())
{
$normalizedData = $this->prepareForDenormalization($data);

$normalizedData = $class::prepareStrangeData($normalizedData);

return parent::denormalize($normalizedData, $class, $format, $context);
}

public function supportsDenormalization($data, $type, $format = null)
{
return $this->isStrangeDataInterface($type);
}
}

interface StrangeDataInterface
{
public static function prepareStrangeData($data): array;
}

class BookShop implements StrangeDataInterface
{
public static function prepareStrangeData($data): array
{
$preparedData = [];

foreach ($data as $key => $value) {
if (preg_match('~^book_[0-9]+$~', $key)) {
$preparedData['books'][] = ['title' => $value];
} else {
$preparedData[$key] = $value;
}
}

return $preparedData;
}

// .... other code hidden
}

function makeSerializer(): Symfony\Component\Serializer\Serializer
{
$extractor = new ReflectionExtractor();

$nameConverter = new CamelCaseToSnakeCaseNameConverter();

$arrayDenormalizer = new ArrayDenormalizer(); // seems to help respect the 'adder' typehints in the model. eg `addEmployee(Employee $employee)`

$strangeDataDenormalizer = new StrangeDataDenormalizer(
null,
$nameConverter,
null,
$extractor
);

$objectNormalizer = new ObjectNormalizer(
null,
$nameConverter,
null,
$extractor
);

$encoder = new JsonEncoder();

$serializer = new Symfony\Component\Serializer\Serializer(
[
$strangeDataDenormalizer,
$objectNormalizer,
$arrayDenormalizer,
],
[$encoder]
);

return $serializer;
}

Is where a build in way to deserialize nested object with type hint property?

This will be supported natively in Symfony 5.1:

The PropertyInfo component extracts information about the properties of PHP classes using several sources (Doctrine metadata, PHP reflection, PHPdoc config, etc.) In Symfony 5.1, we improved this component to also extract information from PHP typed properties.

Before that, you need to give some information to the serializer so it's able to infer the type. A PhpDoc or a typed setter could be enough.

Symfony Serializer Component AbstractNormalizer::CALLBACKS denormalize

I didn't debug your code, instead I made a working version of the deserialization example based on the docs, hope it's serve as guide:

<?php 

include 'vendor/autoload.php';

use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Serializer;

//
//Inializing the Serializer, enconders and callbacks
//
$encoders = [new XmlEncoder(), new JsonEncoder()];
$dateCallback = function ($innerObject, $outerObject, string $attributeName, string $format = null, array $context = []) {
return $innerObject instanceof \DateTime ? $innerObject->format('Y-m-d H:i:sP') : '';
};

$defaultContext = [
AbstractNormalizer::CALLBACKS => [
'createdAt' => $dateCallback,
]
];

$normalizer = [new ObjectNormalizer(null, null, null, null, null, null, $defaultContext)];
$serializer = new Serializer($normalizer, $encoders);

//
//Creating Object Person that will be serialized
//
$person = new Person();
$person->setName('foo');
$person->setAge(99);
$person->setSportsperson(false);
$person->setCreatedAt(new \DateTime());

$arrayContent = $serializer->serialize($person, 'json');

// $arrayContent contains ["name" => "foo","age" => 99,"sportsperson" => false,"createdAt" => null]
//print_r($arrayContent); // or return it in a Response

// Lets deserialize it
$desrializedPerson = $serializer->deserialize($arrayContent, Person::class, 'json');

var_dump($desrializedPerson);

//
// Foo Bar stuff
//

// Entity Person declaration
class Person
{
private $age;
private $name;
private $sportsperson;
private $createdAt;

// Getters
public function getName()
{
return $this->name;
}

public function getAge()
{
return $this->age;
}

public function getCreatedAt()
{
return $this->createdAt;
}

// Issers
public function isSportsperson()
{
return $this->sportsperson;
}

// Setters
public function setName($name)
{
$this->name = $name;
}

public function setAge($age)
{
$this->age = $age;
}

public function setSportsperson($sportsperson)
{
$this->sportsperson = $sportsperson;
}

public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
}
}


Related Topics



Leave a reply



Submit