Skip to content

BC Break on nested object creation #4129

Description

@Exalyon

API Platform version(s) affected: 2.6.3, 2.6.2, 2.6.1, 2.6.0.

Description
I think there is a BC break with the setting of the option deep_object_to_populate to true in here:
// /src/Serializer/SerializerContextBuilder.php

            if ($context['api_allow_update'] && 'PATCH' === $method) {
                $context['deep_object_to_populate'] = $context['deep_object_to_populate'] ?? true;
            }

Before this, the code behaved as this option was false therefore it always created nested objects. Now if there is already another object in place, it will try to update it.

I am not against the change itself if it is wanted. I was suprised by the change in the default behavior happening in a minor version and I simply think it should have been put more forward.

In the related entry in changelog:

PATCH: Support patching deep objects (#3847)

There is no mention of such change in default behavior.

How to reproduce (Minimal test case)

Given the following classes:

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ORM\Entity
 * 
 * @ApiResource(attributes={
 *     "normalization_context"={"groups"={"read"}},
 *     "denormalization_context"={"groups"={"write"}}
 * })
 */
class Foo {
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     * 
     * @Groups({"read"})
     */
    public $id;

    /**
     * @var Bar
     * 
     * @ORM\ManyToOne(targetEntity=Bar::class, cascade={"persist"})
     * 
     * @Groups({"write", "read"})
     */
    public Bar $bar;

    public function __construct (Bar $bar) {
        $this->bar = $bar;
    }

    public function getBar()
    {
        return $this->bar;
    }

    public function setBar(Bar $bar)
    {
        $this->bar = $bar;
    }
}
<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
 
/**
 * @ORM\Entity
 * 
 * @ApiResource(attributes={
 *     "normalization_context"={"groups"={"read"}},
 *     "denormalization_context"={"groups"={"write"}}
 * })
 */
class Bar {
    
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     * 
     * @Groups({"read"})
     */
    public $id;

    /**
     * @ORM\Column(type="string", nullable=true)
     * 
     * @Groups({"write", "read"})
     */
    public string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }
}

Given the following chain of requests :

Request
POST /foos

{
  "bar": {
    "name": "1st"
  }
}

Request:

{
  "@context": "/api/contexts/Foo",
  "@id": "/api/foos/10",
  "@type": "Foo",
  "id": 10,
  "bar": {
    "@id": "/api/bars/10",
    "@type": "Bar",
    "id": 10,
    "name": "1st"
  }
}

Request:

PATCH /api/foos/10
{
  "bar": {
    "name": "2nd"
  }
}

Response:
In 2.5.10:

{
  "@context": "/api/contexts/Foo",
  "@id": "/api/foos/10",
  "@type": "Foo",
  "id": 10,
  "bar": {
    "@id": "/api/bars/11",
    "@type": "Bar",
    "id": 11,
    "name": "2nd"
  }
}

As you can see, there is a creation of a new resource.

In 2.6.3:

{
  "@context": "/api/contexts/Foo",
  "@id": "/api/foos/10",
  "@type": "Foo",
  "id": 10,
  "bar": {
    "@id": "/api/bars/10",
    "@type": "Bar",
    "id": 10,
    "name": "2nd"
  }
}

As you can see, there is an update of the existing resource.

Possible Solution
Workaround : Create a service implementing SerialiserContextBuilderInterface to override this change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions