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
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
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
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
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.