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

Duplicate schemas when referencing with Attributes\Model #2218

Open
oriolvinals opened this issue Feb 15, 2024 · 7 comments
Open

Duplicate schemas when referencing with Attributes\Model #2218

oriolvinals opened this issue Feb 15, 2024 · 7 comments

Comments

@oriolvinals
Copy link

I have an entity called Contact with a serialization group interface named ContactAll

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;

#[ORM\Entity]
class Contact 
{
      #[ORM\Id]
      #[ORM\GeneratedValue]
      #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
      #[Serializer\Groups([SerializationGroup::ContactAll])]
      private ?int $id = null;

      // Other variables, getters and setters...
}

On Controller, I use the open api attributes with model from Nelmio\ApiDocBundle\Annotation\Model

<?php

namespace App\Controller;

use OpenApi\Attributes as OA;

class ContactController extends BaseController 
{
   #[OA\Response(
       response: 200, 
       description: 'Success',
       content: new OA\JsonContent(
           type: 'array', 
           items: OA\Items(ref: new Model(Contact::class, groups: [SerializationGroup::ContactAll]))
       )
    )]
   #[Route(...)]
   public function list(){}

   #[OA\Response(
       response: 200, 
       description: 'Success',
       content: new OA\JsonContent(
           ref: new Model(Contact::class, groups: [SerializationGroup::ContactAll])
       )
    )]
   #[Route(...)]
   public function show(){}

   #[OA\Response(
       response: 201, 
       description: 'Success',
       content: new OA\JsonContent(
           ref: new Model(Contact::class, groups: [SerializationGroup::ContactAll])
       )
    )]
   #[Route(...)]
   public function create(){}

   #[OA\Response(
       response: 200, 
       description: 'Success',
       content: new OA\JsonContent(
           ref: new Model(Contact::class, groups: [SerializationGroup::ContactAll])
       )
    )]
   #[Route(...)]
   public function update(){}
}

When I go to the OpenApi schemas I have 4 same schemas with names Contact, Contact2, Contact3 and Contact4.

There's a way to not duplicate schemas with the same serialization groups?

@oriolvinals oriolvinals changed the title Duplicate references using Model Duplicate schemas when referencing with Attributes\Model Feb 15, 2024
@DjordyKoert
Copy link
Collaborator

I cannot seem to reproduce it. I do see a few typo's in your attributes though.

Incorrect Model usage

new Model(Contact::class, groups: [SerializationGroup::ContactAll])

Should be

new Model(type: Contact::class, groups: [SerializationGroup::ContactAll])

Missing new on OA\Items

items: OA\Items(ref: new Model(Contact::class, groups: [SerializationGroup::ContactAll]))

Should be

items: new OA\Items(ref: new Model(type: Contact::class, groups: [SerializationGroup::ContactAll]))

@oriolvinals
Copy link
Author

@DjordyKoert sorry for the mistakes, I made them without looking at the code, but this is not the problem I really have.

I'm going to start over, since it's harder to reproduce.

I have 3 entities: Member, Contact i Event

Member

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;

#[ORM\Entity]
class Member 
{
      #[ORM\Id]
      #[ORM\GeneratedValue]
      #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
      #[Serializer\Groups([SerializationGroup::MemberAll])]
      private ?int $id = null;

      // Other variables, constructor, getters and setters...
}

Contact

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;

#[ORM\Entity]
class Contact 
{
      #[ORM\Id]
      #[ORM\GeneratedValue]
      #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
      #[Serializer\Groups([SerializationGroup::ContactAll])]
      private ?int $id = null;

      #[ORM\ManyToOne(targetEntity: Member::class)]
      #[Serializer\Ignore]
      private Member $member;

      // Other variables, constructor, getters and setters...
}

Event

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;

#[ORM\Entity]
class Event 
{
      #[ORM\Id]
      #[ORM\GeneratedValue]
      #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
      #[Serializer\Groups([SerializationGroup::EventAll])]
      private ?int $id = null;

      #[ORM\ManyToOne(targetEntity: Member::class)]
      #[Serializer\Ignore]
      private Member $member;

      // Other variables, constructor, getters and setters...
}

Now i create 2 Outputs called ContactOutput i EventOutput with another serialization groups

ContactOutput

<?php

namespace App\Dto\Output;

use App\Entity\Contact;
use App\Entity\Member;
use App\Serialization\Groups\SerializationGroup;
use Symfony\Component\Serializer\Annotation as Serializer;


class ContactOutput
{
    #[Serializer\Groups([SerializationGroup::ContactOutputAll])]
    private readonly Contact $contact;

    #[Serializer\Groups([SerializationGroup::ContactOutputAll])]
    private readonly Member $member;

    public function __construct(Contact $contact, Member $member)
    {
        $this->contact = $contact;
    }

    public function getContact(): Contact
    {
        return $this->contact;
    }

    public function getMember(): Member
    {
        return $this->member;
    }
}

EventOutput

<?php

namespace App\Dto\Output;

use App\Entity\Event;
use App\Entity\Member;
use App\Serialization\Groups\SerializationGroup;
use Symfony\Component\Serializer\Annotation as Serializer;


class EventOutput
{
    #[Serializer\Groups([SerializationGroup::EventOutputAll])]
    private readonly Event $event;

    #[Serializer\Groups([SerializationGroup::EventOutputAll])]
    private readonly Member $member;

    public function __construct(Event $event, Member $member)
    {
        $this->event = $event;
    }

    public function getEventt(): Event
    {
        return $this->event;
    }

    public function getMember(): Member
    {
        return $this->member;
    }
}

Now I want to use this outputs on the contact and event controllers

<?php

namespace App\Controller;

use OpenApi\Attributes as OA;

class ContactController extends BaseController 
{
   #[OA\Response(
       response: 200, 
       description: 'Success',
       content: new OA\JsonContent(
           type: 'array', 
           items: new OA\Items(ref: new Model(type: ContactOutput::class, groups: [
               SerializationGroup::ContactOutputAll,
               SerializationGroup::ContactAll,
               SerializationGroup::MemberAll
           ]))
       )
    )]
   #[Route(...)]
   public function list(){}

   #[OA\Response(
       response: 200, 
       description: 'Success',
       content: new OA\JsonContent(
           ref: new Model(type: ContactOutput::class, groups: [
               SerializationGroup::ContactOutputAll,
               SerializationGroup::ContactAll,
               SerializationGroup::MemberAll
           ])
       )
    )]
   #[Route(...)]
   public function show(){}

   #[OA\Response(
       response: 201, 
       description: 'Success',
       content: new OA\JsonContent(
           ref: new Model(type: ContactOutput::class, groups: [
               SerializationGroup::ContactOutputAll,
               SerializationGroup::ContactAll,
               SerializationGroup::MemberAll
           ])
       )
    )]
   #[Route(...)]
   public function create(){}

   #[OA\Response(
       response: 200, 
       description: 'Success',
       content: new OA\JsonContent(
           ref: new Model(type: ContactOutput::class, groups: [
               SerializationGroup::ContactOutputAll,
               SerializationGroup::ContactAll,
               SerializationGroup::MemberAll
           ])
       )
    )]
   #[Route(...)]
   public function update(){}
}
<?php

namespace App\Controller;

use OpenApi\Attributes as OA;

class EventController extends BaseController 
{
   #[OA\Response(
       response: 200, 
       description: 'Success',
       content: new OA\JsonContent(
           type: 'array', 
           items: new OA\Items(ref: new Model(type: EventOutput::class, groups: [
               SerializationGroup::EventOutputAll,
               SerializationGroup::EventAll,
               SerializationGroup::MemberAll
           ]))
       )
    )]
   #[Route(...)]
   public function list(){}

   #[OA\Response(
       response: 200, 
       description: 'Success',
       content: new OA\JsonContent(
           ref: new Model(type: EventOutput::class, groups: [
               SerializationGroup::EventOutputAll,
               SerializationGroup::EventAll,
               SerializationGroup::MemberAll
           ])
       )
    )]
   #[Route(...)]
   public function show(){}

   #[OA\Response(
       response: 201, 
       description: 'Success',
       content: new OA\JsonContent(
           ref: new Model(type: EventOutput::class, groups: [
               SerializationGroup::EventOutputAll,
               SerializationGroup::EventAll,
               SerializationGroup::MemberAll
           ])
       )
    )]
   #[Route(...)]
   public function create(){}

   #[OA\Response(
       response: 200, 
       description: 'Success',
       content: new OA\JsonContent(
           ref: new Model(type: EventOutput::class, groups: [
               SerializationGroup::EventOutputAll,
               SerializationGroup::EventAll,
               SerializationGroup::MemberAll
           ])
       )
    )]
   #[Route(...)]
   public function update(){}
}

Now in schemas there's a duplicated schemas
image

@emhovis
Copy link

emhovis commented Feb 16, 2024

I have been dealing with the same issue- also using a response object to wraps my entities. I just assumed it was just incorrect usage somewhere else on my end. Coincidentally, this was the root cause for me writing up #2099

image

@DjordyKoert
Copy link
Collaborator

Ah I see why this is happening at least. This happens because Member class is used in both the ContactOutput & EventOutput. Both of these "output" classes get different serialization groups applied:
1.SerializationGroup::ContactOutputAll, SerializationGroup::ContactAll & SerializationGroup::MemberAll
2. SerializationGroup::EventOutputAll, SerializationGroup::EventAll & SerializationGroup::MemberAll

These groups also get passed to the child classes (the Member class in this example). Technically speaking the different groups used could result in a different schema for the Member class, which is the reason why you get a Member & Member2 schema. It just so happens that both of these Schema's are the exact same. https://github.com/DjordyKoert/NelmioApiDocBundle/blob/52b297059b2ccf4bf1db3c8e80b5234f672fe4af/Tests/Functional/Fixtures/Issue2218.json#L216

You can see the setup that I used to replicate it in #2221. I am not yet sure what the proper way would be to change this behaviour or if this should stay as it is.

@mymain
Copy link

mymain commented Feb 18, 2024

@DjordyKoert Same issue on my side.

@aprat84
Copy link

aprat84 commented Mar 12, 2024

@DjordyKoert Maybe an approach would be to only consider the groups that actually affect the model for creating the hash, and therefore unique models.

In the example above, even though we are using 3 groups, the Contact submodel is actually only affected by one of them.

Of course we still need to keep track of the root groups for nested models down the serialization tree.

I tried to look at the code, but it won't be an easy task and sadly now I don't have the time :(

@DjordyKoert
Copy link
Collaborator

Maybe creating an optional swagger-php processor which removes duplicate schemas is an option?

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

No branches or pull requests

5 participants