FriendsOfSymfony / FOSRestBundle

This Bundle provides various tools to rapidly develop RESTful API's with Symfony
http://symfony.com/doc/master/bundles/FOSRestBundle/index.html
MIT License
2.79k stars 707 forks source link

Symfony “fos_rest.request_body” converter can't determine the type of the nested objects #2313

Closed smilesrg closed 2 years ago

smilesrg commented 3 years ago

I faced with the issue where fos_rest.request_body could not detect the type of nested objects. I used MongoDB ODM. Without @var annotation it doesn't detect nested object types, also field type should be iterable and initialized as an ArrayCollection Example:


namespace App\Document;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

/**
* @ODM\Document
*/
class Foo
{
    /**
     * @var NestedFoo[] - this annotation is super important
     *
     * @ODM\Field
     * @Assert\All({
     *     @Assert\Type(NestedFoo::class)
     * })
     * @Assert\Valid()
     */
    private iterable $nestedFoos; //declaring the field as iterable type, as PersistenceCollection is assigned

    public function __construct()
    {
        $this->nestedFoos = new ArrayCollection(); //It is not necessary, but recommended according to the manuals
    }
}

Similar problem and my answer on Stackoverflow: https://stackoverflow.com/questions/60410008/symfony-fos-rest-request-body-converter-do-not-deserialize-nested-dto-classes/68236473#68236473

I think it worth adding to the documentation "how to specify the type of the nested object for a proper deserialization" as it didn't mentioned anywhere, I had to spend half of the day digging this issue.

smilesrg commented 2 years ago

UPD: Put iterable type to a setter, because

    public function setNestedFoos(iterable $foos);

works well, but

    /**
    * @param NestedFoo[] $nestedFoos
    */
    public function setNestedFoos(\Doctrine\Common\Collections\Collection $nestedFoos);

will lead to a type error:

Failed to denormalize attribute "nestedFoos" value for class "App\Document\Foo": Expected argument of type "Doctrine\Common\Collections\Collection", "array" given at property path "nestedFoos".

if you want to use the ArrayCollection type, set it just right in the setter:

    /**
    * @param iterable<NestedFoo> $nestedFoos
    */
    public function setNestedFoos(iterable $nestedFoos)
    {
        if ($nestedFoos instanceof Collection) {
            $this->nestedFoos = $nestedFoos;
        } else {
            $this->nestedFoos = new ArrayCollection($nestedFoos);
        }
    }

You can also want to type-hint your property with the Collection interface, full class listing would look like this:

namespace App\Document;

use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Symfony\Component\Validator\Constraints as Assert;

/**
* @ODM\Document
*/
class Foo
{
    /**
     * @var Collection<NestedFoo>
     *
     * @ODM\Field
     * @Assert\All({
     *     @Assert\Type(NestedFoo::class)
     * })
     * @Assert\Valid()
     */
    private Collection $nestedFoos;

    public function __construct()
    {
        $this->nestedFoos = new ArrayCollection();
    }

    /**
     * @return Collection<NestedFoo>
     */
    public function getNestedFoos(): Collection
    {
        return $this->nestedFoos;
    }

    /**
     * @param iterable<NestedFoo> $nestedFoos
     */
    public function setNestedFoos(iterable $nestedFoos)
    {
        if ($nestedFoos instanceof Collection) {
            $this->nestedFoos = $nestedFoos;
        } else {
            $this->nestedFoos = new ArrayCollection($nestedFoos);
        }
    }
}

I'm not sure if it's better to use ArrayCollection instead of Collection.

goetas commented 2 years ago

this is a symfony or jms serializer question i guess.

The right type hint to use in the property is Collection. In your settrs you can use whatever you prefer but the important thing is that when setting the prop you make it a subclass of Collection (as example ArrayCollection).

On a side note, Your setNestedFoos should avoid $this->nestedFoos = new ArrayCollection((array)$nestedFoos); but rather try to update the collection making a diff of it (otherwise you might get some weird edgecases...)

smilesrg commented 2 years ago

this is a symfony or jms serializer question i guess.

It goes tightly with the “fos_rest.request_body” converter, so I mention that it is worth adding these hints to the docs.

In your settrs you can use whatever you prefer

No. If I use Collection, serializer deserializes a collection of nested entities into the array of objects. And that will lead to a type error.