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

Operation level serialization groups disregarded in parent classes when resource level serialization is defined #2967

Open
tec4 opened this issue Aug 5, 2019 · 7 comments
Assignees
Labels
bug

Comments

@tec4
Copy link

@tec4 tec4 commented Aug 5, 2019

I've noticed the documentation indicates that when serialization groups are specified at an operation level they should take precedence over the configuration specified at the resource level. This makes sense and seems to work fine when your entity does not extend from another class.

Example of what works: Notice that I have resource level normalizationContext and denormalizationContext defined on the CheeseListing entity. But, I also have the operation POST set up to use the serialization group of "create" and have applied that group to the $description property. This works as expected and the Swagger docs generate everything correctly (model section has a "cheeses-create" definition and the $description property is defined).

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
use Symfony\Component\Validator\Constraints as Assert;
use Carbon\Carbon;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;

/**
 * @ApiResource(
 *      collectionOperations={
 *          "get",
 *          "post"={
 *              "normalization_context"={"groups"={"create"}},
 *              "denormalization_context"={"groups"={"create"}},
 *          }
 *      },
 *      itemOperations={
 *          "get",
 *          "put"
 *      },
 *     normalizationContext={"groups"={"cheese_listing:read"}, "swagger_definition_name"="Read"},
 *     denormalizationContext={"groups"={"cheese_listing:write"}, "swagger_definition_name"="Write"},
 *     shortName="cheeses",
 *     attributes={
 *          "pagination_items_per_page"=2,
 *          "formats"={"jsonld", "json", "html", "jsonhal", "csv"={"text/csv"}}
 *     }
 * )
 * @ApiFilter(BooleanFilter::class, properties={"isPublished"})
 * @ApiFilter(SearchFilter::class, properties={"title": "partial", "description": "partial"})
 * @ApiFilter(RangeFilter::class, properties={"price"})
 * @ApiFilter(PropertyFilter::class)
 * @ORM\Entity(repositoryClass="App\Repository\CheeseListingRepository")
 */
class CheeseListing extends BaseEntity
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @Assert\NotBlank()
     * @Assert\Length(
     *     min=2,
     *     max=50,
     *     maxMessage="Describe your cheese in 50 chars or less"
     * )
     * @Groups({"cheese_listing:read", "cheese_listing:write"})
     * @ORM\Column(type="string", length=255)
     */
    private $title;

    /**
     * @Assert\NotBlank()
     * @Groups({"cheese_listing:read", "create"})
     * @ORM\Column(type="text")
     */
    private $description;

But, if I have my CheeseListing extend BaseEntity and the BaseEntity class contains a property that has a serialization group of "create", it does not show up if I keep my CheeseListing as defined above. BUT if I remove the resource level normalizationContext and denormalizationContext lines it shows up then... so not sure why it is completely being ignored.

The 2 lines I needed to remove to get the "create" serialization group to show up for the POST operation. But... with removing those, I now lose my Read and Write definitions and groups.

 *     normalizationContext={"groups"={"cheese_listing:read"}, "swagger_definition_name"="Read"},
 *     denormalizationContext={"groups"={"cheese_listing:write"}, "swagger_definition_name"="Write"},

My BaseEntity.php file:

namespace App\Entity;

use Symfony\Component\Serializer\Annotation\Groups;

class BaseEntity
{
    /**
     * @Groups({"create"})
     */
    protected $someProperty;

    public function getSomeProperty()
    {
        return $this->someProperty;
    }

    public function setSomeProperty($someProperty): void
    {
        $this->someProperty = $someProperty;
    }
}

Any idea why I need to remove the resource level normalization and denormalization contexts to get groups in parent classes to show up?

@tec4

This comment has been minimized.

Copy link
Author

@tec4 tec4 commented Aug 5, 2019

I've just checked and I'm having the same issue when trying to use a BaseEntityTrait with the same code as BaseEntity to try and work around the inheritance. Using the trait, I also need to remove the resource level serialization groups in order to get the $someProperty, with the serialization group of "create" to be respected.

@tec4

This comment has been minimized.

Copy link
Author

@tec4 tec4 commented Aug 7, 2019

I've created a repo to reproduce this issue here. The readme contains simple instructions to build the project and reproduce the issue.

It has one API Resource called "Example" which extends from a simple Base class and includes some simple properties from BaseTrait. They look as follows:

Example Entity

<?php

namespace App\Entity;

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

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"read"}},
 *     denormalizationContext={"groups"={"write"}},
 *     collectionOperations={
 *          "post"={
 *              "normalization_context"={"groups"={"create"}},
 *              "denormalization_context"={"groups"={"create"}},
 *          }
 *     }
 * )
 * @ORM\Entity(repositoryClass="App\Repository\ExampleRepository")
 */
class Example extends Base
{
    use BaseTrait;

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

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

    /**
     * @Groups({"create"})
     * @ORM\Column(type="string", length=255)
     */
    private $createOnlyProperty;

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

    public function getExample(): ?string
    {
        return $this->example;
    }

    public function setExample(string $example): self
    {
        $this->example = $example;

        return $this;
    }

    public function getCreateOnlyProperty(): ?string
    {
        return $this->createOnlyProperty;
    }

    public function setCreateOnlyProperty(string $createOnlyProperty): self
    {
        $this->createOnlyProperty = $createOnlyProperty;

        return $this;
    }
}

Base Class

<?php

namespace App\Entity;

use Symfony\Component\Serializer\Annotation\Groups;

class Base
{
    /**
     * @var string
     * @Groups({"read", "write", "create"})
     */
    private $basePropertySuccess;

    /**
     * @var string
     * @Groups({"create"})
     */
    private $basePropertyFail;

    public function getBasePropertySuccess(): ?string
    {
        return $this->basePropertySuccess;
    }

    public function setBasePropertySuccess(string $basePropertySuccess): self
    {
        $this->basePropertySuccess = $basePropertySuccess;

        return $this;
    }

    public function getBasePropertyFail(): ?string
    {
        return $this->basePropertyFail;
    }

    public function setBasePropertyFail(string $basePropertyFail): self
    {
        $this->basePropertyFail = $basePropertyFail;

        return $this;
    }
}

BaseTrait

<?php

namespace App\Entity;

use Symfony\Component\Serializer\Annotation\Groups;

trait BaseTrait
{
    /**
     * @var string
     * @Groups({"read", "write", "create"})
     */
    private $traitPropertySuccess;

    /**
     * @var string
     * @Groups({"create"})
     */
    private $traitPropertyFail;

    public function getTraitPropertySuccess(): ?string
    {
        return $this->traitPropertySuccess;
    }

    public function setTraitPropertySuccess(string $traitPropertySuccess): self
    {
        $this->traitPropertySuccess = $traitPropertySuccess;

        return $this;
    }

    public function getTraitPropertyFail(): ?string
    {
        return $this->traitPropertyFail;
    }

    public function setTraitPropertyFail(string $traitPropertyFail): self
    {
        $this->traitPropertyFail = $traitPropertyFail;

        return $this;
    }
}

As is, the Models section looks like:
image

But, if I remove the following lines from the Example entity:

 *     normalizationContext={"groups"={"read"}},
 *     denormalizationContext={"groups"={"write"}},

It now looks like:
image

I would have expected that the all of the properties shown under the last image for the Example-create model would be present no matter what since they all include the "create" serialization group.

@teohhanhui

This comment has been minimized.

Copy link
Member

@teohhanhui teohhanhui commented Aug 9, 2019

Yup, definitely looks like a bug to me.

@teohhanhui teohhanhui added the bug label Aug 9, 2019
@teohhanhui

This comment has been minimized.

Copy link
Member

@teohhanhui teohhanhui commented Aug 9, 2019

Does it work as expected when you actually try doing POST /cheese_listings?

Could be a bug with the Swagger DocumentationNormalizer.

@teohhanhui teohhanhui self-assigned this Aug 9, 2019
@tec4

This comment has been minimized.

Copy link
Author

@tec4 tec4 commented Aug 9, 2019

@teohhanhui - Interesting, I actually had not tried to post data to the endpoint since I assumed the docs represented an accurate representation of what could/could not be updated.

I Just tested posting to the http://localhost:8000/api/examples endpoint (this is in the example bug repository I created and linked in my 3rd comment in this issue report) and it looks like the serializer is working correctly and persisting correctly to the DB. As you suspected it does look to be an issue with the translation to the docs itself.

image

image

@teohhanhui

This comment has been minimized.

Copy link
Member

@teohhanhui teohhanhui commented Aug 9, 2019

Thanks for the confirmation. It helps to know where to look for the problem. The code for the Swagger DocumentationNormalizer is quite a minefield, but someone will have to dive in. 🙈

@tec4

This comment has been minimized.

Copy link
Author

@tec4 tec4 commented Aug 9, 2019

Haha thanks @teohhanhui!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
2 participants
You can’t perform that action at this time.