symfony4swagger-phpnelmioapidocbundle

Swagger annotation problems with objects containing objects


I am writing end2end tests for a rest api in a Symfony 4 project.

I use php 7.4, Swagger annotation, nelmio/api-doc-bundle 3.6.1, nelmio/cors-bundle 1.5.6

This is my controller code for the method.

/**
 * @Route(path="", name="create", methods={"POST"})
 * @ValidationGroups({"create"})
 *
 * @SWG\Post(
 *     path="/api/professions",
 *     tags={"Endpoint: Professions"},
 *     summary="save a profession",
 *     @SWG\Parameter(
 *         name="ProfessionDto",
 *         required=true,
 *         in="body",
 *         @Model(type=ProfessionDto::class)
 *     ),
 *     @SWG\Response(
 *         response=201,
 *         description="created",
 *     )
 * )
 */
public function add(ProfessionDto $professionDto): CreatedView
{
    $professionDto = $this->professionService->insert($professionDto);

    return $this->created(['profession' => $professionDto]);
}

ProfessionDto is the object defining the exchanged data. It contains as property some more objects, since I want to have some structure in the returned data and not just a heap of key value pairs.

In the class ProfessionDto I defined the properties, which relate to other objects e.g.:


namespace App\Api\Dto;

use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG;
use Symfony\Component\Serializer\Annotation\Groups as SerializerGroups;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @SWG\Definition()
 */
class ProfessionDto
{
    use HistoryDto;

    /**
     * @SWG\Property(description="Id", type="integer")
     *
     * @SerializerGroups({"view", "update", "collection"})
     */
    public ?int $id = null;

    /**
     * @Assert\NotBlank(groups={"create"})
     * @Assert\Type("string")
     * @SWG\Property(description="...", type="string")
     */
    public string $name;

    /**
     * @Assert\Type("boolean")
     * @SWG\Property(description="ist aktiviert/deaktiviert", type="boolean")
     */
    public bool $active;

    /**
     * @SWG\Property(
     *     description="...",
     *     type="object",
     *     ref=@Model(type=ProfessionAreaDto::class)
     * )
     *
     * @SerializerGroups({"view", "create", "update"})
     */
    public ?ProfessionAreaDto $professionArea = null;

    /**
     * @SWG\Property(
     *     description="...",
     *     type="object",
     *     ref=@Model(type=ProfessionalActivityDto::class)
     * )
     *
     * @SerializerGroups({"view", "create", "update"})
     */
    public ?ProfessionalActivityDto $professionalActivity = null;

    /**
     * @SWG\Property(
     *     description="...",
     *     type="object",
     *     ref=@Model(type=IntroductionDto::class)
     * )
     *
     * @SerializerGroups({"view", "create", "update"})
     */
    public ?IntroductionDto $introduction = null;

    /**
     * @SWG\Property(
     *     description="...",
     *     type="object",
     *     ref=@Model(type=PerformanceBehaviourDto::class)
     * )
     *
     * @SerializerGroups({"view", "create", "update"})
     */
    public ?PerformanceBehaviourDto $performanceBehaviour = null;
}

If I call the api with postman or via my test and pass in my data as json e.g.

{
  "id":null,
  "name":"fancy new data",
  "active":true,
  "professionArea":{
    "id":48,
    "name":"Vitae nulla aperiam aut enim.",
    "active":true,
    "signature":null,
    "signatureTypeId":null,
    "transition":null,
    "infotextCheck":null
  },
  "professionalActivity":{
    "id":null,
    "textMResignationTestimonial":null,
    ...
    "changedBy":null
  },
  "introduction":{
    "id":null,
    "textMResignationTestimonial":null,
    "textMInterimTestimonial":null,
    ...
    "changedOn":null,
    "changedBy":null
  },
  "performanceBehaviour":{
    "id":null,
    "textMResignationGrade1":null,
    "textMInterimGrade1":null,
    ...
    "changedOn":null,
    "changedBy":null
  },
  "createdOn":null,
  "createdBy":null,
  "changedOn":null,
  "changedBy":null
}

I get the error message: TypeError: Typed property App\Api\Dto\ProfessionDto::$professionArea must be an instance of App\Api\Dto\ProfessionAreaDto or null, array used

What did I do wrong? Do I expect too much? are objects within objects not possible?


Solution

  • Since this project didn't use the friendsofsymfony/rest-bundle and the Controller didn't extend the AbstractFOSRestController.

    So the solution to my problem was to manually write a Denormalizer class, which maps my array back to the proper objects.
    This is how I did it:

    
    namespace App\Serializer;
    
    
    use App\Api\Dto\IntroductionDto;
    use App\Api\Dto\PerformanceBehaviourDto;
    use App\Api\Dto\ProfessionalActivityDto;
    use App\Api\Dto\ProfessionAreaDto;
    use App\Api\Dto\ProfessionDto;
    use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
    
    class ProfessionDeNormalizer implements DenormalizerInterface
    {
        public function denormalize($data, $type, $format = null, array $context = [])
        {
            $dto = new ProfessionDto();
            $areaDto = new ProfessionAreaDto();
            $introDto = new IntroductionDto();
            $activityDto = new ProfessionalActivityDto();
            $performanceDto = new PerformanceBehaviourDto();
    
            foreach($data['professionArea'] as $key => $value) {
                $areaDto->$key = $value;
            }
            unset($data['professionArea']);
    
            foreach($data['introduction'] as $key => $value) {
                $introDto->$key = $value;
            }
            unset($data['introduction']);
    
            foreach($data['professionalActivity'] as $key => $value) {
                $areaDto->$key = $value;
            }
            unset($data['professionalActivity']);
    
            foreach($data['performanceBehaviour'] as $key => $value) {
                $areaDto->$key = $value;
            }
            unset($data['performanceBehaviour']);
    
            foreach($data as $key => $value) {
                $dto->$key = $value;
            }
    
            $dto->professionArea = $areaDto;
            $dto->professionalActivity = $activityDto;
            $dto->performanceBehaviour = $performanceDto;
            $dto->introduction = $introDto;
    
            return $dto;
        }
    
        public function supportsDenormalization($data, $type, $format = null)
        {
            return ProfessionDto::class === $type;
        }
    
    }