Symfony Serializer deserialize array into ORM OneToMany relation - symfony

In a Symfony 5 project I get this error
Failed to denormalize attribute "options" value for class "App\Entity\Configuration": Expected argument of type "App\Entity\Option", "array" given at property path "options".
when trying to deserialize a JSON-string.
The JSON-string looks like
{
"options": [
{
"id": 1,
"name": "x1"
},
{
"id": 2,
"name": "x2"
}
]
}
The configuration Entity is
/**
* #ORM\Entity(repositoryClass=ConfigurationRepository::class)
*/
class Configuration
{
// ...
/**
* #ORM\OneToMany(targetEntity=Option::class, mappedBy="configuration", orphanRemoval=true)
*/
private $options;
// ...
}
and the Option Entity
/**
* #ORM\Entity(repositoryClass=OptionRepository::class)
* #ORM\Table(name="`option`")
*/
class Option
{
// ...
/**
* #ORM\ManyToOne(targetEntity=Configuration::class, inversedBy="options")
* #ORM\JoinColumn(nullable=false)
*/
private $configuration;
// ...
}
The serializer part is
// $configData set to the JSON-string near the top of this post
$encoder = new JsonEncoder();
$defaultContext = [
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
return $object->getName();
},
];
$normalizer = new ObjectNormalizer(null, null, null, null, null, null, $defaultContext);
$serializer = new Serializer([$normalizer], [$encoder]);
$configurationDeserialized = $serializer->deserialize( $configData, Configuration::class, 'json' );
My test JSON-string was originaly created by using the Serializer to serialize an existing Configuration object using the settings stated here for the deserialization.
So I guess I am missing some part of the serializer config that tells it how to handle the array -> OneToMany relation (which vice versa it automaticaly encodes)...

Related

Return array of Strings in API Platform

I'm new to API Platform and am trying to clone an existing API. I have an entity Tag that has properties which are an id and a name. The default behaviour for /api/tags is to return an array of objects. Something like:
[
{
"id": 1,
"name": "tag1"
},
{
"id": 2,
"name": "tag2"
}
]
What I actually want in the output is just a simple array of strings:
[
"tag1",
"tag2"
]
From the documentation, it sounds like I need to register a data transformer:
final class TagOutputTransformer implements DataTransformerInterface
{
/**
* {#inheritdoc}
*/
public function transform($data, string $to, array $context = [])
{
return $data->name;
}
/**
* {#inheritdoc}
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
return $data instanceof Tag;
}
}
services.yaml:
services:
'App\DataTransformer\TagOutputTransformer': ~
And annotate my entity:
/**
* #ApiResource(
* collectionOperations={
* "get"={
* "method"="GET",
* "output"="string"
* }
* }
* )
* #ORM\Entity(repositoryClass=TagRepository::class)
*/
The trouble is that string isn't a valid thing to put there in the annotation. I get Class string does not exist. If I remove the quotes, I get Couldn't find constant string.
Is this the right approach? What am I doing wrong?
By specifying "output"="string", you tell API Platform to transform your Tag resource to a string instance for output. string, however, is not a class and can not be instantiated (or transformed to).
What you want is to tell API Platform to transform Tag into a an object that represents your desired data representation.
For example, implement a custom TagCollectionOutput class (holding an array of Tag names) and add "output"=TagCollectionOutput::class to Tag. Then have TagOutputTransformer transform $data into a TagCollectionOutput instance.

How to solve exception NotNormalizableValueException during deserialization array of objects in Serializer Component?

The following error occurs during object deserialization:
Symfony\Component\Serializer\Exception\NotNormalizableValueException
The type of the "products" attribute for class "shop\manage\flexbe\objects\Lead" must be one of "shop\manage\flexbe\objects\Product[]" ("array" given).
Have JSON-object:
{
"id": "9757241",
"time": "1567105530",
// other params
"products": [
{
"title": "Product name",
"count": 1
}
]
}
In the Lead class that describes the object related methods to "products":
private $products = [];
/**
* #return Product[]
*/
public function getProducts()
{
return $this->products;
}
/**
* #param Product $product
*/
public function addProduct(Product $product): void
{
$this->products[] = $product;
}
Deserialization Code:
$normalizer = new ObjectNormalizer(null, null, new PropertyAccessor(), new ReflectionExtractor());
$serializer = new Serializer(array($normalizer), array(new JsonEncoder()));
$lead = $serializer->deserialize($data, Lead::class, 'json');
I can’t understand what the problem is. It is expected using the addProduct() method deserializer should bypass the array and add all objects to Product class like in this case.
I decided using an ArrayDenormalizer, and a setter with PHPDoc indicating the data type, an array of Products [] objects. But why the method with addProduct () did not work - the question remains.
Deserialization:
$encoder = [new JsonEncoder()];
$extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]);
$normalizer = [new ArrayDenormalizer(), new ObjectNormalizer(null, null, null, $extractor)];
$serializer = new Serializer($normalizer, $encoder);
/** #var $lead Lead */
$lead = $serializer->deserialize($data,Lead::class,'json');
Setter for products in the Lead class:
/**
* #param Product[] $products
*/
public function setProducts(array $products)
{
$this->products = $products;
}

Symfony denormalization: Recursive Denormalization not working

I'am trying to desirialize the following Json structure into a Profile object
{
"label": "lorem label",
"info": {
"name": "lorem name",
"title": "lorem title"
}
}
I have a class Profile
namespace App\Document;
class profile
{
/**
* #var string
*/
protected $label;
/**
* #var Info
*/
protected $info;
// getters and setters
}
and a class Info
namespace App\Document;
class Info
{
/**
* #var string
*/
protected name;
/**
* #var string
*/
protected title;
// getters & setters
}
my controller's code is the following
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use App\Document;
public function addProfile(Request $request, DocumentManager $dm, $profile_id)
{
$phpDocNormalizer = new ObjectNormalizer(null, null, null, new PhpDocExtractor());
$reflectionNormalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
$serializer = new Serializer([$phpDocNormalizer, $reflectionNormalizer], [new JsonEncoder()]);
$profile = $serializer->deserialize($request->getContent(), Profile::class, 'json');
return new JsonResponse(Response::HTTP_CREATED);
}
As the symfony docs says, i have the phpDocExtractor set and working, and the doc annotations are set too, the PropertyInfo Component is installed too. But i keep getting this NotNormalizableValueException:
The type of the "info" attribute for class "App\Document\Profile" must be one of "App\Document\Info" ("array" given).
I've been stuck with this one for a while now, any help is much appreciated.
public function addProfile(Request $request, DocumentManager $dm, $profile_id)
{
$extractors = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
$normalizer = new ObjectNormalizer(null, null, null, $extractors);
$serializer = new Serializer([$normalizer], [new JsonEncoder()]);
$profile = $serializer->deserialize($request->getContent(), Profile::class, 'json');
return new JsonResponse(Response::HTTP_CREATED);
}
You can find a more detailed example here : https://symfony.com/doc/current/components/property_info.html#usage

JMS serialisation listener: added objects are empty

I am building Symfony3 project. I am using JMS for serialisation. I have Group entity with ManyToMany relationship to User. I am trying to add additional data to Group objects. I am using serialisation listener to achieve this. For example, I want to get group members for that group who went to same school as current user. I setData to group object like this:
class SerializationListener{
//Constructor
/**
* #param ObjectEvent $event
*/
public function onGroupEntitySerialize(ObjectEvent $event)
{
if (!$this->currentUser) {
return;
}
/** #var GenericSerializationVisitor $visitor */
$visitor = $event->getVisitor();
$group = $event->getObject();
$groupMembersFromUserSchool = $this->em->getRepository('AppBundle:User')
->groupMembersFromUserSchool($group, $this->currentUser);
$visitor->setData('members', $groupMembersFromUserSchool);
$groupMembersCountFromUserSchool = count($groupMembersFromUserSchool);
$visitor->setData('memberCount', $groupMembersCountFromUserSchool);
}
}
However, I get this as result:
{
"items": [
{
"memberCount": 17,
"id": 1,
"name": "TestGroup1",
"members": [
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{}
]
}
]
}
So user details in members property are hidden. How do I display them?
class Group{
/**
* #Expose()
* #Groups({"group-details", "group-list"})
*/
protected $members;
}
class User{
/**
* #Expose()
* #Groups({"group-details", "group-list"})
*/
private $id;
/**
* #Expose()
* #Groups({"group-details", "group-list"})
*/
protected $name;
}
Referring to the doc of the source code here you can only add integer|float|boolean|string|array value:
/**
* Allows you to add additional data to the current object/root element.
* #deprecated use setData instead
* #param string $key
* #param integer|float|boolean|string|array|null $value This value must either be a regular scalar, or an array.
* It must not contain any objects anymore.
*/
public function addData($key, $value)
{
if (isset($this->data[$key])) {
throw new InvalidArgumentException(sprintf('There is already data for "%s".', $key));
}
So try something like:
$arrayValuesOfMembers = // some data as array probably extracted by the repo methods
$visitor->setData('members', $arrayValuesOfMembers);
Hope this help

deserializing json to embedded object in Symfony 3.2 - Expected argument of type "Acme\StoreBundle\Document\Gps", "array" given

Acme\StoreBundle\Document\Person
/**
* #MongoDB\Document
*/
class Person
{
/**
* #MongoDB\bool
*/
private $hasemail;
/**
* #MongoDB\EmbedOne(targetDocument="Gps")
*/
private $gps;
/**
* #MongoDB\Field(name="email", type="collection")
*/
private $email;
}
...
Acme\StoreBundle\Document\Gps
/**
* #MongoDB\EmbeddedDocument
*/
class Gps
{
/**
* #MongoDB\Field(type="float")
*/
private $latitude;
/**
* #MongoDB\Field(type="float")
*/
private $longitude;
}
...
mongo json document
{
"hasemail": true,
"gps": {
"latitude": 42.941579990394,
"longitude": -85.244641161525
},
"email": [
"sdfgsdfg#sfgsdfg.org",
"sdfgsdfg#fgsdfg.com",
"sdfgsdfg#sdfgsdfg.com"
]
}
serializing mongo document to json works perfect. but deserializing json to document throws an error: "Expected argument of type 'Acme\StoreBundle\Document\Gps', 'array' given"
deserialize code:
$post = $request->getContent();
$serializer = $this->get('serializer');
$person = $serializer->deserialize($post, Person::class, 'json');
so finally I used ReflectionExtractor and it worked like charm. No custom denormalizers necessary.
http://symfony.com/doc/current/components/serializer.html#recursive-denormalization-and-type-safety
$post = $request->getContent();
$normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); //
$serializer = new Serializer(array($normalizer, new DateTimeNormalizer(\DateTime::ISO8601), new ObjectNormalizer()), [new JsonEncoder()]);
$person = $serializer->deserialize($post, Person::class, 'json');
$dm = $this->get('doctrine_mongodb')->getManager();
$dm->persist($person);
$dm->flush();
return new Response('Created id ' . $person->getId());
Your $post is likely an array and not a raw JSON object as you're expecting.
[{
key: value
}]
is different from..
{
key: value
}
Separately, there looks to be a separate issue based on your error message and the example code you have here.
Acme\StoreBundle\Document\Gps is the expected class, while deserializing Voter::class so you may want to check your namespaces and class names as well.

Resources