Serializing api outputs with Symfony serializer
Tags: php, doctrine
Published: 2023-02-08 17:57:25

I've seen api's where, after getting results from a doctrine query, the resulted entities are serialized and returned as the http response.

Following that way could generate some problems because you are coupling your database schema to your views and you would return information client should not see. Let's see an example

Let's imagine we have an entity like this:

```php
  #[ORM\Entity(repositoryClass: UserRepository::class)]
  class User {

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $name;

    #[ORM\Column(length: 255)]
    private string $lastName;

    #[ORM\Column]
    private \DateTimeImmutable $birthDate;
    
    // getters & setters
}
```
And now, let's imagine we query _UserRepository_ and return it to client

```php
   class UserController extends AbstractController {
      
      #[Route('/users', name: 'get_users', methods: ['GET'])]
      public function getUsersAction(EntityManagerInterface $em, SerializerInterface $serializer): JsonResponse
      {
          $users = $em->getRepository(UserRepository::class)->findAll();
          return new JsonResponse($serializer->normalize($users), 200);
      }

   }
```
Following this way we would be exposing all user properties even those that we do not want to expose. 

To fix this, we can relie on two ways:

1.- Adding serializer groups to entity properties
2.- Using a service which builds an output

As an example, let's consider we don't want to expose id property. 

### Adding serializer groups to entity properties

In this case, we only have to add serializer groups to entity properties we want to expose. Then, when serializing results we must indicate serializer group in order to serialize only properties which match that group

```php
   #[ORM\Entity(repositoryClass: UserRepository::class)]
   class User {

      #[ORM\Id]
      #[ORM\GeneratedValue]
      #[ORM\Column]
      private ?int $id = null;

      #[ORM\Column(length: 255)]
      #[Groups(['api_output'])]
      private string $name;

      #[ORM\Column(length: 255)]
      #[Groups(['api_output'])]
      private string $lastName;

      #[ORM\Column]
      #[Groups(['api_output'])]
      private \DateTimeImmutable $birthDate;
    
      // getters & setters
  }
```
We've added _api_output_ group to _name_, _lastName_ and _birthDate_ properties. Let's serialize now telling serializer which group to use.

```php
   class UserController extends AbstractController {
      
      #[Route('/users', name: 'get_users', methods: ['GET'])]
      public function getUsersAction(EntityManagerInterface $em, SerializerInterface $serializer): JsonResponse
      {
          $users = $em->getRepository(UserRepository::class)->findAll();

          $context = (new ObjectNormalizerContextBuilder())->withGroups('api_output')->toArray();
          return new JsonResponse($serializer->normalize($users, null, $context), 200);
      }

   }
```

I don't really like that approach since you are still coupling your database schema to your outputs. If your schema starts growing and have associations, managing groups can get complicated and it would be easy to get circular reference errors.

So, Let's see the second approach

### Using a service which builds an output

This last approach consist in creating a separate service which will be in charge of building the output. To do this, we will need:

- A separate model (_UserOutput_) which will act as an output model
- A service which will receive an array of users and will return an array or _UserOutput_

Let's see it:

```php
   class UserOutput {

       public function __construct(
          public readonly string $name,
          public readonly string $lastName,
          public readonly string $birthDate
       ){}
   }
```

```php
   class UserOutputBuilder
   {
       /**
        * @param User[] $users
        * @return UserOutput[]
        */
       public function buildOutput(array $users): array
       {
           $targetUsers = [];
           foreach ($users as $user){
              $targetUsers[] = new UserOutput(
                 $user->getName(),
                 $user->getLastname(),
                 $user->getBirthDate()->format('d/m/Y')
              );
           }

           return $targetUsers;
       }
   }
```

Now we only have to delegate responsibility of building the output on _UserOutputBuilder_.

```php
   class UserController extends AbstractController {

       #[Route('/users', name: 'get_users', methods: ['GET'])]
       public function getUsersAction(EntityManagerInterface $em, UserOutputBuilder $userOutputBuilder, SerializerInterface $serializer): JsonResponse
       {
           $users = $em->getRepository(UserRepository::class)->findAll();
           return new JsonResponse($serializer->normalize($userOutputBuilder->buildOutput($users)), 200);
       }

  }
```

With this approach, any change of our schema will not affect to our output since there is a builder which builds the output from doctrine results and it uses a separate model. If our schema change and we would want to expose more properties, we will have to add it to the output model and make the changes needed on the builder.

We've learned the importance of decoupling entities of our api outputs so that database schema changes does not transfer problems to our outputs (for instance serializing circular references). In my recently published book, I show the how to serialize api outputs and also how to deserialize request api inputs into an operation request model. You can find the book [here](https://www.amazon.com/dp/B0D79KGL7P) to learn more.