Serialization groups let you choose which properties to include when serializing an object. You might use one group for public API responses and another for admin exports. Symfony 8.2 adds new ways to configure these groups, and lets controllers and Messenger use named serializers.

Serialized Names and Paths per Group

Sergey Danilchenko
Contributed by Sergey Danilchenko in #58236

Until now, the #[SerializedName] and #[SerializedPath] attributes defined a single name or path for a property, regardless of the serialization group. Using different keys for the same property required a workaround, such as creating separate DTOs.

In Symfony 8.2, both attributes are repeatable and accept a groups argument. For example, if version 2 of your API renames a field, version 1 can keep the old name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\SerializedName;

class Product
{
    #[Groups(['api_v1', 'api_v2'])]
    #[SerializedName('product_name', groups: 'api_v1')]
    public string $name;
}

$serializer->serialize($product, 'json', ['groups' => ['api_v1']]);
// {"product_name": "..."}

$serializer->serialize($product, 'json', ['groups' => ['api_v2']]);
// {"name": "..."}

This also works when denormalizing: with the api_v1 group, the serializer reads the value from product_name. A name or path defined without groups applies to all groups and serves as a fallback when none of the declared groups is in the context. You can also configure this in YAML and XML mapping files.

Ignoring Groups

zim32
Contributed by zim32 in #57166

The #[Ignore] attribute always excludes a property from serialization. Sometimes you only want to exclude it in certain contexts: for example, keeping a token when serializing an object for internal use, while leaving it out of HTTP responses.

Symfony 8.2 adds the AbstractNormalizer::IGNORED_GROUPS context option (ignored_groups) to exclude the properties that belong to the given groups:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

class Customer
{
    public function __construct(
        public readonly int $id,
        #[Groups(['internal'])]
        public readonly string $token,
    ) {
    }
}

$serializer->serialize($customer, 'json', [
    AbstractNormalizer::IGNORED_GROUPS => ['internal'],
]);
// {"id": 42}

If you use context builders, call the new withIgnoredGroups() method instead.

Opt-in Default Groups

Bastien Clément
Contributed by Bastien Clément in #63998

When you serialize an object with groups, properties without a #[Groups] attribute are left out. If you're used to the Validator component, you might expect them to belong to the Default group and the group named after the class (e.g. Book).

Symfony 8.2 adds the AbstractNormalizer::ENABLE_DEFAULT_GROUPS context option (enable_default_groups) to apply the same convention in the Serializer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Symfony\Component\Serializer\Attribute\Groups;

class Book
{
    #[Groups(['Default', 'admin'])]
    public string $title = 'Dune';

    public string $isbn = '978-0441013593';

    #[Groups(['admin'])]
    public int $stock = 12;
}

$serializer->normalize($book, context: ['groups' => ['Default']]);
// ['title' => 'Dune']

$serializer->normalize($book, context: ['groups' => ['Default'], 'enable_default_groups' => true]);
// ['title' => 'Dune', 'isbn' => '978-0441013593']

// the class short name works the same as 'Default'
$serializer->normalize($book, context: ['groups' => ['Book'], 'enable_default_groups' => true]);
// ['title' => 'Dune', 'isbn' => '978-0441013593']

These implicit groups only apply when you don't request any custom group. In the example above, using ['groups' => ['admin']] still leaves out $isbn. To enable this behavior across your application, set the option in the default serializer context:

1
2
3
4
5
# config/packages/serializer.yaml
framework:
    serializer:
        default_context:
            enable_default_groups: true

Named Serializers for Controllers and Messenger

HypeMC
Contributed by HypeMC in #66189 and #66192

Named serializers let you define serializers with their own name converter, default context and normalizers. Until now, #[MapRequestPayload], #[MapQueryString] and #[Serialize] always used the default serializer service. So did Messenger's messenger.transport.symfony_serializer. Changing that serializer's settings for your API could therefore also change the format of your Messenger messages.

In Symfony 8.2, you can choose a serializer for each of these features. This example uses snake_case names for the API and a separate serializer for messages:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# config/packages/framework.yaml
framework:
    serializer:
        named_serializers:
            api:
                name_converter: serializer.name_converter.camel_case_to_snake_case
            messages: ~

    # used by #[MapRequestPayload] and #[MapQueryString]
    request:
        serializer: serializer.api
    # used by #[Serialize]
    response:
        serializer: serializer.api

    messenger:
        serializer:
            default_serializer: messenger.transport.symfony_serializer
            symfony_serializer:
                service: serializer.messages

Validation errors (422 responses) still use the default serializer. Their property paths won't use your named serializer's name converter.

Published in #Living on the edge