I have two entities, Question and Alternative where Question has a OneToMany relation with Alternative and I'm trying to send a JSON with a nested document of Alternative on it via POST to Question API Platform.
The API Platform returns that error below :
Nested documents for "alternatives" attribute are not allowed. Use IRIs instead.
Searching about it I've found some people saying that is only possible using IRIs and some other people saying that is possible to use Denormalization and Normalization contexts to solve this problem but I can't find some example or tutorial about it.
TL;DR;
Is there a way to send a nested relation into an entity POST on API Platform without using IRIs?
UPDATE:
As asked, please see below the two mappings of Question and Alternative entities.
Question
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\QuestionRepository")
* @ApiResource()
*/
class Question implements CreatedAtEntityInterface, UpdatedAtEntityInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Token", inversedBy="questions")
* @ORM\JoinColumn(nullable=false)
* @Assert\NotBlank()
*/
private $token;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Question", inversedBy="question_versions")
*/
private $question;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Question", mappedBy="question")
*/
private $question_versions;
/**
* @ORM\Column(type="integer")
* @Assert\NotBlank()
*/
private $version;
/**
* @ORM\Column(type="string", length=100)
* @Assert\Length(max="100")
* @Assert\NotBlank()
*
*/
private $name;
/**
* @ORM\Column(type="integer")
* @Assert\NotBlank()
*/
private $type;
/**
* @ORM\Column(type="text", length=65535)
* @Assert\NotBlank()
* @Assert\Length(max="65535")
*/
private $enunciation;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @Assert\Length(max="255")
*/
private $material;
/**
* @ORM\Column(type="text", length=65535, nullable=true)
* @Assert\Length(max="65535")
*/
private $tags;
/**
* @ORM\Column(type="boolean")
* @Assert\NotNull()
*/
private $public;
/**
* @ORM\Column(type="boolean")
* @Assert\NotNull()
*/
private $enabled;
/**
* @ORM\Column(type="datetime")
* @Assert\DateTime()
*/
private $createdAt;
/**
* @ORM\Column(type="datetime")
* @Assert\DateTime()
*/
private $updatedAt;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Alternative", mappedBy="question")
*/
private $alternatives;
/**
* @ORM\OneToMany(targetEntity="App\Entity\QuestionProperty", mappedBy="question")
*/
private $properties;
/**
* @ORM\OneToMany(targetEntity="App\Entity\QuestionAbility", mappedBy="question")
*/
private $abilities;
/**
* @ORM\OneToMany(targetEntity="QuestionCompetency", mappedBy="question")
*/
private $competencies;
}
Alternative
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\AlternativeRepository")
* @ORM\Table(name="alternatives")
* @ApiResource()
*/
class Alternative implements CreatedAtEntityInterface, UpdatedAtEntityInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Question", inversedBy="alternatives")
* @ORM\JoinColumn(nullable=false)
* @Assert\NotBlank()
*/
private $question;
/**
* @ORM\Column(type="text", length=65535)
* @Assert\NotBlank()
* @Assert\Length(max="65535")
*/
private $enunciation;
/**
* @ORM\Column(type="datetime")
* @Assert\DateTime()
*/
private $createdAt;
/**
* @ORM\Column(type="datetime")
* @Assert\DateTime()
*/
private $updatedAt;
/**
* @ORM\OneToMany(targetEntity="App\Entity\AlternativeProperty", mappedBy="alternatives")
*/
private $properties;
}
With Entity Framework 6, nested entities and complex types are recognized. In the above code, you can see that Person is nested within the Student class. When you use the Entity Framework Power Tool to show how Entity Framework interprets the model this time, there's true Identity property, and Person complex type.
In case the API user required more information about nested resources or lists of sub-resources, they could simply make another API call. But, is this always the best solution? To find the answer, we carried out a client analysis to ascertain what clients’ needs were in different cases.
In the above code, you can see that Person is nested within the Student class. When you use the Entity Framework Power Tool to show how Entity Framework interprets the model this time, there's true Identity property, and Person complex type. So Entity Framework will persist that data.
If you reference a new entity from the navigation property of an entity that is already tracked by the context, the entity will be discovered and inserted into the database. In the following example, the post entity is inserted because it is added to the Posts property of the blog entity which was fetched from the database.
you can try implement Denormalization
Question:
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\QuestionRepository")
* @ApiResource(
* denormalizationContext={"groups"={"post"}}
* )
*/
class Question implements CreatedAtEntityInterface, UpdatedAtEntityInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Token", inversedBy="questions")
* @ORM\JoinColumn(nullable=false)
* @Assert\NotBlank()
* @Groups({"post"})
*/
private $token;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Question", inversedBy="question_versions")
* @Groups({"post"})
*/
private $question;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Question", mappedBy="question")
* @Groups({"post"})
*/
private $question_versions;
/**
* @ORM\Column(type="integer")
* @Assert\NotBlank()
* @Groups({"post"})
*/
private $version;
/**
* @ORM\Column(type="string", length=100)
* @Assert\Length(max="100")
* @Assert\NotBlank()
* @Groups({"post"})
*/
private $name;
/**
* @ORM\Column(type="integer")
* @Assert\NotBlank()
* @Groups({"post"})
*/
private $type;
/**
* @ORM\Column(type="text", length=65535)
* @Assert\NotBlank()
* @Assert\Length(max="65535")
* @Groups({"post"})
*/
private $enunciation;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @Assert\Length(max="255")
* @Groups({"post"})
*/
private $material;
/**
* @ORM\Column(type="text", length=65535, nullable=true)
* @Assert\Length(max="65535")
* @Groups({"post"})
*/
private $tags;
/**
* @ORM\Column(type="boolean")
* @Assert\NotNull()
* @Groups({"post"})
*/
private $public;
/**
* @ORM\Column(type="boolean")
* @Assert\NotNull()
* @Groups({"post"})
*/
private $enabled;
/**
* @ORM\Column(type="datetime")
* @Assert\DateTime()
* @Groups({"post"})
*/
private $createdAt;
/**
* @ORM\Column(type="datetime")
* @Assert\DateTime()
* @Groups({"post"})
*/
private $updatedAt;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Alternative", mappedBy="question")
* @Groups({"post"})
*/
private $alternatives;
/**
* @ORM\OneToMany(targetEntity="App\Entity\QuestionProperty", mappedBy="question")
* @Groups({"post"})
*/
private $properties;
/**
* @ORM\OneToMany(targetEntity="App\Entity\QuestionAbility", mappedBy="question")
* @Groups({"post"})
*/
private $abilities;
/**
* @ORM\OneToMany(targetEntity="QuestionCompetency", mappedBy="question")
* @Groups({"post"})
*/
private $competencies;
}
Alternative:
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\AlternativeRepository")
* @ORM\Table(name="alternatives")
* @ApiResource()
*/
class Alternative implements CreatedAtEntityInterface, UpdatedAtEntityInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Question", inversedBy="alternatives")
* @ORM\JoinColumn(nullable=false)
* @Assert\NotBlank()
* @Groups({"post"})
*/
private $question;
/**
* @ORM\Column(type="text", length=65535)
* @Assert\NotBlank()
* @Assert\Length(max="65535")
* @Groups({"post"})
*/
private $enunciation;
/**
* @ORM\Column(type="datetime")
* @Assert\DateTime()
* @Groups({"post"})
*/
private $createdAt;
/**
* @ORM\Column(type="datetime")
* @Assert\DateTime()
* @Groups({"post"})
*/
private $updatedAt;
/**
* @ORM\OneToMany(targetEntity="App\Entity\AlternativeProperty", mappedBy="alternatives")
* @Groups({"post"})
*/
private $properties;
}
Now you can this send JSON to POST/PUT without IRI:
{
"token": "/api/tokens/1",
"question": "string",
"version": "string",
"name": "string",
"type": 0,
"enunciation": "string",
"public": true,
"enabled": true,
"alternatives": [
{
"enunciation": "String",
"createdAt": "2018-01-01 11:11:11",
"updatedAt": "2018-01-01 11:11:11"
}
]
}
For anyone who has a problem with the error cascade persist you have to add it in the OneToMany property.That is, for our question:
@ORM\OneToMany(targetEntity="App\Entity\Alternative", mappedBy="question", cascade={"persist"})
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