I'm trying to deserialize an entity with a relationship using the symfony serializer component. This is my entity:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Document
*
* @ORM\Table(name="document")
* @ORM\Entity(repositoryClass="AppBundle\Repository\DocumentRepository")
*/
class Document
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="Genre", inversedBy="documents")
* @ORM\JoinColumn(name="id_genre", referencedColumnName="id")
*/
private $genre;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=100)
*/
private $name;
//getters and setters down here
...
}
And the Genre entity:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Genre
*
* @ORM\Table(name="genre")
* @ORM\Entity(repositoryClass="AppBundle\Repository\GenreRepository")
*/
class Genre
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=50, nullable=true)
*/
private $name;
/**
* @ORM\OneToMany(targetEntity="Document", mappedBy="genre")
*/
private $documents;
public function __construct()
{
$this->documents= new ArrayCollection();
}
//getters and setters down here
....
}
In my controller action right now I'm trying this:
$encoders = array(new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$serializer = new Serializer($normalizers, $encoders);
$document = $serializer->deserialize($request->getContent(), 'AppBundle\Entity\Document', 'json');
And my json data:
{"name": "My document", "genre": {"id": 1, "name": "My genre"}}
But I got the next error:
Expected argument of type "AppBundle\Entity\Genre", "array" given (500 Internal Server Error)
Is possible to deserialize a json request with an entity with relations inside?
Thanks in advace.
Yes and no. First, you shouldn't re-create a new instance of the serializer in your controller but use the serializer
service instead.
Second, no it's not possible out of the box with Symfony serializer. We are doing it in https://api-platform.com/ but there is a bit of magic there. That said, a PR has been made to support it: https://github.com/symfony/symfony/pull/19277
For anyone who is working on this in '18. I've managed to get this working using two different approaches.
The associated entities I'm working with.
class Category
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", name="name", length=45, unique=true)
*/
private $name;
}
class Item
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", name="uuid", length=36, unique=true)
*/
private $uuid;
/**
* @ORM\Column(type="string", name="name", length=100)
*/
private $name;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Category", fetch="EAGER")
* @ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false)
*/
private $category;
}
Method 1: Using Form Classes
#ItemType.php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use App\Entity\Category;
use App\Entity\Item;
class ItemType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'name',
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Item::class,
));
}
}
#ItemController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use App\Entity\Item;
use App\Form\ItemType;
class ItemController extends BaseEntityController
{
protected $entityClass = Item::class;
/**
* @Route("/items", methods="POST")
*/
public function createAction(Request $request)
{
$data = $request->getContent();
$item = new Item();
$form = $this->createForm(ItemType::class, $item);
$decoded = $this->get('serializer')->decode($data, 'json');
$form->submit($decoded);
$object = $form->getData();
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($object);
$entityManager->flush();
return $this->generateDataResponse("response text", 201);
}
}
Method 2: A Custom Normalizer
The PropertyInfo Component needs to be enabled.
#/config/packages/framework.yaml
framework:
property_info:
enabled: true
Register the custom normalizer.
#/config/services.yaml
services:
entity_normalizer:
class: App\SupportClasses\EntityNormalizer
public: false
autowire: true
autoconfigure: true
tags: [serializer.normalizer]
The custom normalizer.
#EntityNormalizer.php
namespace App\SupportClasses;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class EntityNormalizer extends ObjectNormalizer
{
protected $entityManager;
public function __construct(
EntityManagerInterface $entityManager,
?ClassMetadataFactoryInterface $classMetadataFactory = null,
?NameConverterInterface $nameConverter = null,
?PropertyAccessorInterface $propertyAccessor = null,
?PropertyTypeExtractorInterface $propertyTypeExtractor = null
) {
$this->entityManager = $entityManager;
parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor);
}
public function supportsDenormalization($data, $type, $format = null)
{
return (strpos($type, 'App\\Entity\\') === 0) &&
(is_numeric($data) || is_string($data) || (is_array($data) && isset($data['id'])));
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->entityManager->find($class, $data);
}
}
Our controller's create action.
#ItemController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use App\Entity\Item;
use App\Form\ItemType;
class ItemController extends BaseEntityController
{
protected $entityClass = Item::class;
/**
* @Route("/items", methods="POST")
*/
public function createAction(Request $request)
{
$data = $request->getContent();
$object = $this->get('serializer')->deserialize($data, $this->entityClass, 'json');
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($object);
$entityManager->flush();
return $this->generateDataResponse('response text', 201);
}
}
This has worked for me. I received inspiration from: https://medium.com/@maartendeboer/using-the-symfony-serializer-with-doctrine-relations-69ecb17e6ebd
I modified the normalizer to allow me to send the category as a child json object which is converted to a child array when the data is decoded from json. Hopefully this helps someone.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With