sajad torkamani

Why would you dynamically change the serialization context?

Suppose you have a Book entity where most of its field can be managed by any user, but some can be managed only by admin users:

// api/src/Entity/Book.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

    normalizationContext: ['groups' => ['book:output']],
    denormalizationContext: ['groups' => ['book:input']],
class Book
    // ...

     * This field can be managed only by an admin
    #[Groups(["book:output", "admin:input"])]
    public bool $active = false;

     * This field can be managed by any user
    #[Groups(["book:output", "book:input"])]
    public string $name;

    // ...

Notice the admin:input serialization group. We want to detect if the current user is an admin, and if so, dynamically add the admin:input value to the deserialization context. This way, we can restrict reading or writing of specific fields only to admins.

Dynamically set the normalization/denormalization context for all instances of an entity

API Platform provides you with a ContextBuilder, that can prepare the context for serialization and deserialization. You can decorate the ContextBuilder service to override its createFromRequest method:

# api/config/services.yaml
    # ...
        decorates: 'api_platform.serializer.context_builder'
        arguments: [ '@App\Serializer\BookContextBuilder.inner' ]
        autoconfigure: false

This tells the Symfony service container that the App\Serializer\BookContextBuilder service replaces the default context builder service with the id api_platform.serializer.context_builder.

The arguments option will pass the given arguments to the decorating service:

// api/src/Serializer/BookContextBuilder.php

namespace App\Serializer;

use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use App\Entity\Book;

final class BookContextBuilder implements SerializerContextBuilderInterface
    private $decorated;
    private $authorizationChecker;

    public function __construct(SerializerContextBuilderInterface $decorated, AuthorizationCheckerInterface $authorizationChecker)
        $this->decorated = $decorated;
        $this->authorizationChecker = $authorizationChecker;

    public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
        $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
        $resourceClass = $context['resource_class'] ?? null;

        if ($resourceClass === Book::class && isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_ADMIN') && false === $normalization) {
            $context['groups'][] = 'admin:input';

        return $context;

Now, if the subject is an instance of Book , the user has ROLE_ADMIN and the context is for denormalization (instead of normalization), then the admin:input gorup will be dynamically added to the denormalization context.

Dynamically set the normalization/denormalization context on a per-item basis

Suppose you want to add certain serialization groups only if the current user has access to the given book. For example, you might want to add a can_retrieve_book serialization group if the user is the owner of the book.

You can do this by registering a custom normalizer that conditionally adds a serialization group based on some logic:

// api/src/Serializer/BookAttributeNormalizer.php

namespace App\Serializer;

use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;

class BookAttributeNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
    use NormalizerAwareTrait;


    private $tokenStorage;

    public function __construct(TokenStorageInterface $tokenStorage)
        $this->tokenStorage = $tokenStorage;

    public function normalize($object, $format = null, array $context = [])
        if ($this->userHasPermissionsForBook($object)) {
            $context['groups'][] = 'can_retrieve_book';

        $context[self::ALREADY_CALLED] = true;

        return $this->normalizer->normalize($object, $format, $context);

    public function supportsNormalization($data, $format = null, array $context = [])
        // Make sure we're not called twice
        if (isset($context[self::ALREADY_CALLED])) {
            return false;

        return $data instanceof Book;

    private function userHasPermissionsForBook($object): bool
        // Get permissions from user in $this->tokenStorage
        // for the current $object (book) and
        // return true or false

The logic for checking if the normalizer is already called helps prevent recursive calls.