core icon indicating copy to clipboard operation
core copied to clipboard

Serialization of ArrayPaginator includes unwanted keys from \LimitIterator

Open johnpancoast opened this issue 3 years ago • 0 comments

API Platform version(s) affected: 2.6.8

Description
Getting unexpected serialized JSON output when using a custom data provider that returns an ArrayPaginator. I'm not sure if I'm missing something but after some digging here's what I found.

First, Symfony's normalizer creates a normalized array based off of the keys and values of the data being normalized. If the data being normalized is an associative array or has keys that don't start from 0 then the resulting normalized data will include those keys, otherwise it will be a standard normalized array. This is expected behavior, however...

... Since the ArrayPaginator uses a \LimitIterator, when Symfony's normalizer iterates the keys and values from the ArrayPaginator, the keys of the items being returned from the \LimitIterator are what get output. This causes the normalized/serialized data of an ArrayPaginator to include unwanted keys in collection output.

How to reproduce

  • Create an API resource class with some pagination settings for testing:
<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;

#[ApiResource(
    attributes: [
        "pagination_enabled" => true,
        "pagination_items_per_page" => 2,
    ]
)]
class Foo
{
    #[ApiProperty(identifier: true)]
    private int|float $id;

    private string $name;

    /**
     * @param float|int $id
     * @param string    $name
     */
    public function __construct(float|int $id, string $name)
    {
        $this->id = $id;
        $this->name = $name;
    }

    /**
     * @return float|int
     */
    public function getId(): float|int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }
}
  • Create an ArrayPaginator object with a collection of the above objects and limit the results then serialize it.

// ...

use ApiPlatform\Core\DataProvider\ArrayPaginator;

$firstResult = 2;
$maxResults = 2;

$data = new ArrayPaginator([
    new Foo(1, 'foo-1'),
    new Foo(2, 'foo-2'),
    new Foo(3, 'foo-3'),
    new Foo(4, 'foo-4'),
    new Foo(5, 'foo-5'),
], $firstResult, $maxResults);


echo $serializer->serialize($data, 'json');

The above will output the following JSON (note the unwanted keys which came from the \LimitIterator):

{
   "2":{
      "id":3,
      "name":"foo-3"
   },
   "3":{
      "id":4,
      "name":"foo-4"
   }
}

Possible Solution
End-users can possibly use a custom serializer or normalizer. Another option is for the lib's ArrayPaginator to instead use an extension of \LimitIterator similar to the following:

<?php

declare(strict_types=1);

namespace App\Foo;

/**
 * Custom limit iterator that outputs 0'th based keys
 */
class LimitIterator extends \LimitIterator
{
    private int $keyPosition = 0;

    /**
     * @inheritDoc
     */
    public function key()
    {
        return $this->keyPosition;
    }

    /**
     * @inheritDoc
     */
    public function next()
    {
        $this->keyPosition ++;

        parent::next();
    }

    /**
     * @inheritDoc
     */
    public function rewind()
    {
        $this->keyPosition = 0;

        parent::rewind();
    }
}

If I copy API Platform's ArrayPaginator and have it use the above LimitIterator, then the same test as above returns data as expected:

// A copy of ArrayPaginator that uses the above LimitIterator
use App\Api\Data\ArrayPaginator;

$firstResult = 2;
$maxResults = 2;

$data = new ArrayPaginator([
    new Foo(1, 'foo-1'),
    new Foo(2, 'foo-2'),
    new Foo(3, 'foo-3'),
    new Foo(4, 'foo-4'),
    new Foo(5, 'foo-5'),
], $firstResult, $maxResults);


echo $serializer->serialize($data, 'json');

The above will output the data as expected without the keys:

[
   {
      "id":3,
      "name":"foo-3"
   },
   {
      "id":4,
      "name":"foo-4"
   }
]

johnpancoast avatar Apr 27 '22 22:04 johnpancoast