Un groupe de sérialisation est une étiquette posée sur une propriété avec #[Groups] pour choisir quelles propriétés le Serializer Symfony inclut dans une sortie donnée. Symfony 8.2, attendu en novembre 2026, rend ces groupes nettement plus souples : noms différents selon le groupe, exclusion de groupes à l'appel, groupes par défaut en opt-in, et serializers nommés branchés sur les contrôleurs et Messenger. Le billet officiel du 1er octobre 2026 regroupe cinq PR (#58236, #57166, #63998, #66189, #66192).
Ton API v2 renomme product_name en name, et la v1 doit continuer à répondre product_name pendant des mois. Jusqu'ici, tu avais deux sorties : dupliquer le DTO, avec deux classes promises à diverger au premier ajout de champ, ou écrire un normalizer maison qui renomme les clés à la main. Symfony 8.2 règle ce cas, et trois autres frictions qu'on traîne depuis longtemps.
Comment renommer un champ selon le groupe avec #[SerializedName]
Depuis Symfony 8.2, #[SerializedName] et #[SerializedPath] sont répétables et acceptent un argument groups (PR #58236 de Sergey Danilchenko, qui répond à une issue ouverte depuis 2019). Une même propriété porte son nom v1 et son nom v2, et le groupe demandé à l'appel tranche :
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": "..."}- La dénormalisation est symétrique : avec le groupe
api_v1, le serializer lit la valeur dansproduct_name. Tes payloads entrants v1 passent sans code supplémentaire. - Un nom déclaré sans groupe s’applique à tous les groupes et sert de repli quand aucun groupe déclaré n’est dans le contexte.
- Les mappings YAML et XML sont supportés, si tu évites les attributs sur tes entités.
Ce que ça ne fait pas : décider quand la v1 meurt. Mais le coût de la maintenir tombe à une ligne par champ renommé. Si tu exposes ton API avec API Platform, la logique reste la même côté sérialisation ; notre article sur API Platform et les providers/processors couvre le reste de la chaîne.
Comment exclure un groupe seulement dans certains contextes avec ignored_groups
#[Ignore] exclut toujours une propriété ; ignored_groups l'exclut seulement dans le contexte demandé. La nouvelle option AbstractNormalizer::IGNORED_GROUPS (PR #57166) sert le cas typique : garder un token pour un usage interne, le retirer des réponses HTTP.
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}Si tu passes par les context builders, la méthode équivalente est withIgnoredGroups(). C'est un filet de sécurité utile côté réponses publiques, mais garde en tête que c'est une liste noire : un champ sensible oublié dans le mauvais groupe reste exposé. Ne remplace pas ta liste blanche de groupes par ce mécanisme, ajoute-le par-dessus.
Que fait enable_default_groups sur les propriétés sans #[Groups] ?
Comportement actuel : dès que tu demandes des groupes, toute propriété sans #[Groups] disparaît. Le Validator fait l'inverse : une contrainte sans groupe appartient à Default et au groupe du nom court de la classe. L'option opt-in AbstractNormalizer::ENABLE_DEFAULT_GROUPS (PR #63998 de Bastien Clément) applique la même convention au Serializer :
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']Votre dette technique s'accumule ?
Audit complet en 10 jours. Recommandations priorisées et actionnables.
Le point facile à rater : ces groupes implicites ne s'appliquent que si tu ne demandes aucun groupe personnalisé. Avec ['groups' => ['admin']], $isbn reste exclu. Pour activer l'option partout, passe par le contexte par défaut du serializer :
# config/packages/serializer.yaml
framework:
serializer:
default_context:
enable_default_groups: trueC'est opt-in, donc sans rupture. Mais l'activer globalement élargit ce qui sort pour les contextes Default ou nom de classe : des propriétés jamais pensées pour être exposées peuvent apparaître. Fais un inventaire de tes réponses, avec des tests de snapshot, avant de le basculer.
Comment utiliser un serializer nommé pour l’API et un autre pour Messenger
Les serializers nommés ont leur propre name converter, leur contexte par défaut et leurs normalizers. Ce qui manquait, c'était de les brancher au bon endroit : #[MapRequestPayload], #[MapQueryString], #[Serialize] et le transport Messenger utilisaient tous le service serializer par défaut. Conséquence concrète : passer ton API en snake_case pouvait changer le format de tes messages Messenger, alors que tes consommateurs attendent un format stable. Les PR #66189 et #66192 de HypeMC règlent ça :
# 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.messagesUne limite, écrite dans le billet comme dans la PR : les erreurs de validation (réponses 422) passent toujours par le serializer par défaut. Leurs chemins de propriété n'utilisent pas le name converter de ton serializer nommé : avec une API en snake_case, une erreur peut nommer un champ en camelCase. Prévois un test de contrat sur ce cas. Et si tu changes le serializer d'un transport, les messages déjà en file doivent rester lisibles par tes consommateurs : garde la même configuration effective au moment du basculement. Pour la fiabilité des envois eux-mêmes, voir l'outbox et le claim check de Messenger 8.2.
Pièges à vérifier avant d’activer ces options
- Groupes déclarés deux fois : pour un nom par version, la propriété doit appartenir aux groupes dans
#[Groups]et dans#[SerializedName(..., groups: ...)]. Le nom sans groupe sert de repli quand aucun groupe de version n’est demandé. - ignored_groups et groupes implicites : selon la PR #63998, avec
enable_default_groupsactif, ignorerDefaultexclut aussi toutes les propriétés sans groupe. Combiner les deux options demande donc un test dédié. - Serializers nommés et service par défaut : d’après la discussion de la PR #66189, les serializers nommés sont créés comme enfants du service
serializer. Sans lui, pas de serializer nommé configuré par le framework. - Normalizers maison : la PR #58236 touche
MetadataAwareNameConverteret le normalizer d’objets. Si tu as surchargé ces classes ou écrit un loader de mapping, relance tes tests de sérialisation sur la branche 8.2.
Ce que ça change pour ton projet
Aucune des cinq PR n'annonce de dépréciation : tout est additif. Côté performance, ces options s'ajoutent aux gains du Serializer de Symfony 8.2 sur les listes. Mon ordre d'adoption :
- Adopte les serializers nommés dès la sortie, même sans toucher aux groupes : découpler l’API de Messenger supprime une classe entière d’incidents silencieux.
- Utilise les noms par groupe pour tes prochaines évolutions d’API, pas pour une réécriture en masse des DTO dupliqués qui fonctionnent.
- Laisse
enable_default_groupsdésactivé tant que tu n’as pas de tests de non-régression sur tes réponses. - Garde
ignored_groupscomme garde-fou, jamais comme mécanisme principal de protection des données.
Avant d'activer enable_default_groups ou de changer le serializer de ton API, fais l'inventaire de ce qui sort déjà de tes endpoints : champs internes, groupes oubliés, formats partagés avec Messenger. Si tu veux un regard extérieur sur ce point, un Bear Scan couvre ces risques d'exposition de données en dix jours et te rend des recommandations priorisées.

Écrit par
Flavien Métivier
Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.
Mon parcours