core: Unexpected non-iterable value for to-many relation

Hi,

Updated to the latest version (2.5.2), and now I’m getting the following error :

Symfony\Component\Serializer\Exception\UnexpectedValueException: Unexpected non-iterable value for to-many relation.
#9 /vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php(526): ApiPlatform\Core\Serializer\AbstractItemNormalizer::getAttributeValue
#8 /vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php(181): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::normalize
#7 /vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php(151): ApiPlatform\Core\Serializer\AbstractItemNormalizer::normalize
#6 /vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php(152): Symfony\Component\Serializer\Serializer::normalize
#5 /vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php(125): Symfony\Component\Serializer\Serializer::serialize
#4 /src/AppBundle/Controller/JobController.php(90): AppBundle\Controller\JobController::createJobAction
#3 /vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php(151): Symfony\Component\HttpKernel\HttpKernel::handleRaw
#2 /vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php(68): Symfony\Component\HttpKernel\HttpKernel::handle
#1 /vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php(198): Symfony\Component\HttpKernel\Kernel::handle
#0 /public/index.php(24): null

Any idea why ?

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 31 (22 by maintainers)

Commits related to this issue

Most upvoted comments

Still, it’s not a collection, and before it was returning something correct and now not. I understand that this is an underlying bug (or feature, I will check) in Symfony not an APIP one.

I’m in a position where I can investigate and fix thing. So it’s already old news and it’s fixed since a long time on my side. But 2.5.2 is a minor release, at least 3 persons experience it and the only information we got is Unexpected non-iterable value for to-many relation. Maybe we should specify in the error where it happens property X of entity Y is supposed to be a collection but it's not

@teohhanhui the type is an array, that IS a collection and the target entity is a resource, just a computed one, not from a “doctrine related” collection. So not a bug at all.

Something like that:

class MyEntity
{
    /**
     * @var Collection|Workspace[]
     *
     * @ORM\ManyToMany(targetEntity="Workspace", inversedBy="XXXX")
     */
    private $workspaces;

    /**
     * @var Workspace[]
     */
    private $filteredWorkspaces;


    public function getWorkspace()
    {
        return $this->workspaces->first();
    }

    public function getWorkspaces()
    {
        return $this->workspaces;
    }

    public function getFilteredWorkspaces()
    {
        // do something that finished like that, null or array.
        return random() ? null : [$workspace1, $workspace2];
    }
} 

Maybe we should have return an empty array, but it’s still a regression.

Turns out only InheritedPropertyNameCollectionFactory causes trouble. I fixed it in my project by copying the class and flippling the arguments for is_subclass_of

    App\Metadata\InheritedPropertyNameCollectionFactory:
        decorates: api_platform.metadata.property.name_collection_factory
        arguments:
            - "@api_platform.metadata.resource.name_collection_factory"
            - "@api_platform.metadata.property.name_collection_factory.inherited.inner"

Closing as this is not a bug.

it seems that on another of our projects, it breaks because the property (supposed to be an array) is null, so not iterable.

For the record, I had the same problem when upgrading to api-platform 2.5 and 2.6. I did not had the problem with 2.4. My Symfony property info version is 5.2.4 .

For my case, it is a “problem” in Symfony PropertyInfo. The problem is that my class has a “adder” that has the same name that my getter:

class Order {
  public function getInvoiceList() {
    // …
  }
  public function addInvoice(Invoice $invoice) {
    // …
  }
  public function getInvoice(): Invoice { // this method is deprecated but kept for retro-compatibility
    // …
  }
}

Renaming the method addInvoice to addInvoiceList did fix the problem.

It is reproductible with the following snippet :

<?php

require 'vendor/autoload.php';

use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;

class Invoice {}

class Order
{
    public function getInvoiceList(): void
    {
        // …
    }

    public function addInvoice(Invoice $invoice): void
    {
        // …
    }

    public function getInvoice(): Invoice
    { // this method is deprecated but kept for retro-compatibility
      // …
      return new Invoice();
    }
}

class Order2
{
    public function getInvoiceList(): void
    {
        // …
    }

    public function addInvoiceList(Invoice $invoice): void
    {
        // …
    }

    public function getInvoice(): Invoice
    { // this method is deprecated but kept for retro-compatibility
      // …
      return new Invoice();
    }
}

$reflectionExtractor = new ReflectionExtractor();

$propertyInfo = new PropertyInfoExtractor(
    [$reflectionExtractor],
    [$reflectionExtractor],
    [],
    [$reflectionExtractor],
    [$reflectionExtractor]
);

// see below for more examples
dump($propertyInfo->getTypes(Order::class, 'invoice')[0]); // builtinType is "array"
dump($propertyInfo->getTypes(Order2::class, 'invoice')[0]); // builtinType is "object"

@teohhanhui So yeah, on my side, it’s because the following field is null :

    /**
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\Candidate", mappedBy="offer", cascade={"persist", "remove"})
     * @ORM\JoinColumn(nullable=true, onDelete="CASCADE")
     */
    protected $candidates;

If in the construct method of the entity I do the following, then it works :

$this->candidates = new ArrayCollection();

I’m having the same problem with Api Platform. I think the problem in my case is with the doctrine entity inheritance I have a base entity Account, and an AdminAccount that inherits from Account.
AdminAccount has a manytomany relationship.
I made a little repository to reproduce the bug. https://github.com/nferrand/apiplatform-inheritance-issue

The exception occurs on the list of account entities but not on the list of AdminAccount entities.

@teohhanhui you can consider it’s a regression so.

It worked before as expected (returning a Workspace) and now throw an error. Maybe, if not a collection, just bypass that piece of code ?

Hmm, just checking in on this error.

class MyEntity
{
    /**
     * @var Collection|Workspace[]
     *
     * @ORM\ManyToMany(targetEntity="Workspace", inversedBy="XXXX")
     */
    private $workspaces;

    public function getWorkspace()
    {
        return $this->workspaces->first();
    }

    public function getWorkspaces()
    {
        return $this->workspaces;
    }

} 
App\Entity\MyEntity:
  attributes:
    workspace:
      groups: ['myEntity:read']
    workspaces:
      groups: ['myEntity:read']

If I rename my method to firstWorkspace and change it in the serialization, it works. I find it to suspect that I tried with another collection, just creating a method that is the singular of the collection name and bam.

Here $type is seen as an array for workspace but $attributeValue is a Workspace as it should be. So not iterable, and it crashes with Unexpected non-iterable value for to-many relation..

I’ve attached the swagger docs and the v2.5.1 json output for the document thats failing.

Archive.zip

FYI We’re using MongoDB (Doctrine ODM).

Output message below:

{
  "message": "Unexpected non-iterable value for to-many relation.",
  "status": 400,
  "title": "Bad Request",
  "trace": [
    "Line 176: \\ApiPlatform\\Core\\Serializer\\AbstractItemNormalizer::getAttributeValue",
    "Line 151: \\Symfony\\Component\\Serializer\\Normalizer\\AbstractObjectNormalizer::normalize",
    "Line 146: \\ApiPlatform\\Core\\Serializer\\AbstractItemNormalizer::normalize",
    "Line 119: \\Symfony\\Component\\Serializer\\Serializer::normalize",
    "Line 95: \\Symfony\\Component\\Serializer\\Serializer::serialize",
    "Line 126: \\ApiPlatform\\Core\\EventListener\\SerializeListener::onKernelView",
    "Line 264: \\Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener::__invoke",
    "Line 239: \\Symfony\\Component\\EventDispatcher\\EventDispatcher::doDispatch",
    "Line 73: \\Symfony\\Component\\EventDispatcher\\EventDispatcher::callListeners",
    "Line 168: \\Symfony\\Component\\EventDispatcher\\EventDispatcher::dispatch",
    "Line 151: \\Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher::dispatch",
    "Line 68: \\Symfony\\Component\\HttpKernel\\HttpKernel::handleRaw",
    "Line 201: \\Symfony\\Component\\HttpKernel\\HttpKernel::handle",
    "Line 28: \\Symfony\\Component\\HttpKernel\\Kernel::handle"
  ]
}