Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialization groups are ignored on write #4252

Closed
lebadapetru opened this issue Apr 26, 2021 · 10 comments
Closed

Serialization groups are ignored on write #4252

lebadapetru opened this issue Apr 26, 2021 · 10 comments

Comments

@lebadapetru
Copy link

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)

@guilliamxavier
Copy link
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
Copy link
Author

lebadapetru commented Apr 28, 2021

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
Copy link
Contributor

guilliamxavier commented Apr 28, 2021

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
Copy link
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
Copy link
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
Copy link

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
Copy link

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
Copy link
Member

Probably solved by 2.6.5.

@stale
Copy link

stale bot commented Nov 5, 2022

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 bot added the wontfix label Nov 5, 2022
@stale
Copy link

stale bot commented Jan 4, 2023

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 bot added the stale label Jan 4, 2023
@stale stale bot closed this as completed Jan 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants