Skip to content

Serialization groups are ignored on write #4252

@lebadapetru

Description

@lebadapetru

API Platform version(s) affected: 2.6

Description
I've defined a write group for my category entity which is completely ignored on POST requests. The problem persist on the documentation (openapi) page. The fields that have only read groups are being present on the POST request but the values are ignored upon send.

How to reproduce

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\CategoriesRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"category: read"}},
 *     denormalizationContext={"groups"={"category: write"}},
 *     attributes={}
 * )
 * @ORM\Entity(repositoryClass=CategoriesRepository::class)
 * @ORM\Table("`categories`")
 * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=true, hardDelete=true)
 */
class Category
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     * @Groups({"category: read", "category: write"})
     */
    private int $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"category: read", "category: write"})
     */
    private string $title;

    /**
     * @ORM\Column(type="text", nullable=true)
     * @Groups({"category: read", "category: write"})
     */
    private ?string $description;

    /**
     * @ORM\Column(type="boolean")
     * @Groups({"category: read", "category: write"})
     */
    private bool $isPublic = true;

    /**
     * @ORM\Column(name="deleted_at", type="datetime", nullable=true)
     * @Groups({"category: read"})
     */
    private ?\DateTimeInterface $deletedAt;

    /**
     * @Gedmo\Timestampable(on="update")
     * @ORM\Column(type="datetime")
     * @Groups({"category: read"})
     */
    private ?\DateTimeInterface $updatedAt;

    /**
     * @Gedmo\Timestampable(on="create")
     * @ORM\Column(type="datetime")
     * @Groups({"category: read"})
     */
    private ?\DateTimeInterface $createdAt;

    /**
     * @ORM\ManyToMany(targetEntity=Product::class, mappedBy="categories")
     * @Groups({"category: read", "category: write"})
     */
    private $products;

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

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): self
    {
        $this->title = $title;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): self
    {
        $this->description = $description;

        return $this;
    }

    public function getIsPublic(): ?bool
    {
        return $this->isPublic;
    }

    public function setIsPublic(bool $isPublic): self
    {
        $this->isPublic = $isPublic;

        return $this;
    }

    public function getDeletedAt(): ?\DateTimeInterface
    {
        return $this->deletedAt;
    }

    public function setDeletedAt(?\DateTimeInterface $deletedAt): self
    {
        $this->deletedAt = $deletedAt;

        return $this;
    }

    public function getUpdatedAt(): ?\DateTimeInterface
    {
        return $this->updatedAt;
    }

    public function setUpdatedAt(\DateTimeInterface $updatedAt): self
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }

    public function getCreatedAt(): ?\DateTimeInterface
    {
        return $this->createdAt;
    }

    public function setCreatedAt(\DateTimeInterface $createdAt): self
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    /**
     * @return Collection|Product[]
     */
    public function getProducts(): Collection
    {
        return $this->products;
    }

    public function addProduct(Product $product): self
    {
        if (!$this->products->contains($product)) {
            $this->products[] = $product;
            $product->addCategory($this);
        }

        return $this;
    }

    public function removeProduct(Product $product): self
    {
        if ($this->products->removeElement($product)) {
            $product->removeCategory($this);
        }

        return $this;
    }
}

Additional Context

{
  "title": "string",
  "description": "string",
  "deletedAt": "2021-04-26T19:47:27.683Z",
  "updatedAt": "2021-04-26T19:47:27.683Z",
  "createdAt": "2021-04-26T19:47:27.683Z",
  "products": [
    "string"
  ]
}

The above is the example body from the POST request, which should not have the deletedAt, updatedAt, createdAt fields that are in the read group only. If i'm changing the value of any of those three, it will be ignored and the default value will be used ( in this case the current datetime)

Activity

guilliamxavier

guilliamxavier commented on Apr 27, 2021

@guilliamxavier
Contributor

That's the first time I see spaces in group names, could this be related? 🤔 Does the problem persist after renaming them to spaceless "category:read" / "category:write" (and clearing the cache)?

lebadapetru

lebadapetru commented on Apr 28, 2021

@lebadapetru
Author

That's the first time I see spaces in group names, could this be related? 🤔 Does the problem persist after renaming them to spaceless "category:read" / "category:write" (and clearing the cache)?

@guilliamxavier , i removed the spaces yet the problem persist

If i change the category:read to category:write on the private ?\DateTimeInterface $deletedAt; prop, it disappear from the POST request body, but also from the GET response ( this one is right ).

guilliamxavier

guilliamxavier commented on Apr 28, 2021

@guilliamxavier
Contributor

Weird indeed... (BTW I also just noticed you have the write group on id although no setter)

Another random thought: could there be an interaction with the @Gedmo annotations? 😕

lebadapetru

lebadapetru commented on Apr 28, 2021

@lebadapetru
Author

Yeah, no reason behind that, it just slipped in. I thought about Gedmo too, but i removed it and the problem is still there...
Can it be a bug on the newest version? Is anyone else experiencing this?

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\CategoriesRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"category:read"}},
 *     denormalizationContext={"groups"={"category:write"}},
 *     attributes={}
 * )
 * @ORM\Entity(repositoryClass=CategoriesRepository::class)
 * @ORM\Table("`categories`")
 */
class Category
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     * @Groups({"category:read"})
     */
    private int $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"category:read", "category:write"})
     */
    private string $title;

    /**
     * @ORM\Column(type="text", nullable=true)
     * @Groups({"category:read", "category:write"})
     */
    private ?string $description;

    /**
     * @ORM\Column(type="boolean")
     * @Groups({"category:read", "category:write"})
     */
    private bool $isPublic = true;

    /**
     * @ORM\Column(name="deleted_at", type="datetime", nullable=true)
     * @Groups({"category:read"})
     */
    private ?\DateTimeInterface $deletedAt;

    /**
     * @ORM\Column(type="datetime")
     * @Groups({"category:read"})
     */
    private ?\DateTimeInterface $updatedAt;

    /**
     * @ORM\Column(type="datetime")
     * @Groups({"category:read"})
     */
    private ?\DateTimeInterface $createdAt;

    /**
     * @ORM\ManyToMany(targetEntity=Product::class, mappedBy="categories")
     * @Groups({"category:read", "category:write"})
     */
    private $products;

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

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): self
    {
        $this->title = $title;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): self
    {
        $this->description = $description;

        return $this;
    }

    public function getIsPublic(): ?bool
    {
        return $this->isPublic;
    }

    public function setIsPublic(bool $isPublic): self
    {
        $this->isPublic = $isPublic;

        return $this;
    }

    public function getDeletedAt(): ?\DateTimeInterface
    {
        return $this->deletedAt;
    }

    public function setDeletedAt(?\DateTimeInterface $deletedAt): self
    {
        $this->deletedAt = $deletedAt;

        return $this;
    }

    public function getUpdatedAt(): ?\DateTimeInterface
    {
        return $this->updatedAt;
    }

    public function setUpdatedAt(\DateTimeInterface $updatedAt): self
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }

    public function getCreatedAt(): ?\DateTimeInterface
    {
        return $this->createdAt;
    }

    public function setCreatedAt(\DateTimeInterface $createdAt): self
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    /**
     * @return Collection|Product[]
     */
    public function getProducts(): Collection
    {
        return $this->products;
    }

    public function addProduct(Product $product): self
    {
        if (!$this->products->contains($product)) {
            $this->products[] = $product;
            $product->addCategory($this);
        }

        return $this;
    }

    public function removeProduct(Product $product): self
    {
        if ($this->products->removeElement($product)) {
            $product->removeCategory($this);
        }

        return $this;
    }
}

lebadapetru

lebadapetru commented on Apr 28, 2021

@lebadapetru
Author

Here's the apiplatform config, just in case:

api_platform:
    mapping:
        paths: ['%kernel.project_dir%/src/Entity']
    patch_formats:
        json: ['application/merge-patch+json']
    swagger:
        versions: [3]
    defaults:
        pagination_items_per_page: 30
        items_per_page_parameter_name: 'itemsPerPage'
        pagination_client_items_per_page: true
    formats:
        jsonld:
            mime_types: [ 'application/ld+json' ]

        json:
            mime_types: [ 'application/json' ]

        html:
            mime_types: [ 'text/html' ]

        multipart:
            mime_types: [ 'multipart/form-data' ]
soulcodex

soulcodex commented on Apr 29, 2021

@soulcodex

I have the same trouble on POST endpoints the request body have a weird behavior for POST and PUT endpoints definitions using YAML or Annotations 😭

Looks like the OpenApiFactory cant read the groups from Symfony Serializer

nickvanderzwet

nickvanderzwet commented on May 25, 2021

@nickvanderzwet

Maybe related, I'm not sure: #3668

The Read/Write models are already made for the foreign object (in your case product) and skipped for the actual entity..

It seems like it only affects the api config, responses seems to be okay.

alanpoulain

alanpoulain commented on Jun 23, 2021

@alanpoulain
Member

Probably solved by 2.6.5.

stale

stale commented on Nov 5, 2022

@stale

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale

stale commented on Jan 4, 2023

@stale

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @guilliamxavier@alanpoulain@nickvanderzwet@lebadapetru@soulcodex

        Issue actions

          Serialization groups are ignored on write · Issue #4252 · api-platform/core