1. Introduction
Une référence cyclique (ou circulaire) survient quand un objet contient directement ou indirectement une référence vers un autre objet qui, au final, fait référence au premier.
Exemple concret
Faisons un exemple concret pour une bibliothèque. Supposons qu'on ait la classe Book avec une propriété Author, et que la classe Author ait une propriété Books de type List<Book>, pour qu'il connaisse toutes ses livres.
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
public List<Book> Books { get; set; } = new List<Book>();
}
public class Book
{
public string Title { get; set; }
public Author Author { get; set; }
}
Maintenant, si on crée un auteur et un livre, qu'on les relie, on obtient un "cercle fermé" :
var author = new Author { Name = "Marcel Proust", BirthYear = 1871 };
var book = new Book { Title = "Du côté de chez Swann", Author = author };
author.Books.Add(book);
// Voilà, maintenant author référence book, et book référence author !
Pourquoi c'est un problème ?
Quand vous sérialisez un tel objet en JSON, le sérialiseur parcourt les propriétés. Il voit que l'auteur a des livres, à l'intérieur desquels il y a encore l'auteur... qui contient encore des livres... et ainsi de suite jusqu'à l'infini.
author -> books[] -> author -> books[] ...
C'est comme se regarder dans un miroir en face d'un autre miroir — les reflets partent à l'infini. Sauf qu'au lieu de beaux reflets on obtient un débordement de pile (StackOverflowException).
2. Comment réagit le sérialiseur aux références cycliques ?
Erreur de sérialisation
Par défaut System.Text.Json ne gère pas les références cycliques. Si vous essayez de sérialiser une telle structure, vous obtiendrez l'exception JsonException : "A possible object cycle was detected".
Exemple qui provoquera l'erreur :
string json = JsonSerializer.Serialize(author); // Paf ! JsonException
Visualisation :
graph TD;
Author --> Book;
Book --> Author;
3. Comment résoudre le problème des références cycliques ?
Voyons plusieurs approches réelles, chacune avec ses avantages et inconvénients. Comme vous l'imaginez, il n'y a pas de "flag magique" universel (dommage).
Supprimer les cycles avant la sérialisation
La solution la plus simple — ne pas envoyer les références qui créent le cycle. Avant la sérialisation, mettre à null (ou ignorer) les références problématiques.
À quoi ça ressemble en pratique :
// On enlève temporairement la référence de l'auteur vers ses livres
var authorToSerialize = new Author
{
Name = author.Name,
BirthYear = author.BirthYear,
Books = null // ou ne pas inclure la propriété du tout
};
string json = JsonSerializer.Serialize(authorToSerialize);
// Maintenant tout se sérialise sans problème !
Avantages : simple, rapide, évident.
Inconvénients : vous perdez une partie des données (après désérialisation vous ne retrouverez pas la liaison inverse).
Utiliser l'attribut [JsonIgnore]
On peut marquer la propriété qui participe au cycle comme ignorée :
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
[JsonIgnore]
public List<Book> Books { get; set; }
}
Désormais, lors de la sérialisation de l'auteur, ses livres ne seront pas inclus. C'est similaire à la méthode précédente, mais déclaratif et sans nettoyage manuel.
Avantages : plus simple, moins de risques d'oublier de nettoyer.
Inconvénients : l'information sur les livres de l'auteur est perdue dans le JSON.
Utiliser des identifiants au lieu d'objets imbriqués
Si vous tenez à conserver les deux côtés de la relation (auteurs et livres) sans créer de cycles, utilisez des identifiants uniques au lieu d'objets imbriqués :
public class Book
{
public string Title { get; set; }
public int AuthorId { get; set; } // au lieu d'Author
}
public class Author
{
public int AuthorId { get; set; }
public string Name { get; set; }
// ne stocke pas les books ou stocke la liste de leurs Id
}
En JSON vous aurez des identifiants au lieu d'objets imbriqués. C'est une approche courante en base de données, REST API et systèmes avec des références univoques.
Avantages : pas de cycles, JSON compact, les relations peuvent être reconstituées via les Id.
Inconvénients : le modèle objet habituel est moins naturel, et la désérialisation nécessite une recherche par Id.
Mini-tableau de comparaison :
| Approche | Cycle résolu ? | Perte de données ? | Applicabilité |
|---|---|---|---|
| [JsonIgnore] | Oui | Oui | Quand l'imbrication n'est pas critique |
| Supprimer la référence manuellement | Oui | Oui | Rapide avant la sérialisation |
| Stocker l'Id au lieu de l'objet | Oui* | Non* | REST, bases de données, systèmes complexes |
* Les données ne sont pas perdues, mais pas immédiatement accessibles (recherche par Id nécessaire).
4. Comment configurer System.Text.Json pour sérialiser les références cycliques ?
Depuis .NET 5, JsonSerializerOptions propose un mode de gestion des références : options.ReferenceHandler = ReferenceHandler.Preserve.
Ce mode utilise des champs spéciaux $id et $ref pour les objets réutilisés.
Exemple
var options = new JsonSerializerOptions
{
WriteIndented = true,
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve
};
string json = JsonSerializer.Serialize(author, options);
Console.WriteLine(json);
Le JSON résultant ressemblera à ceci :
{
"$id": "1",
"Name": "Marcel Proust",
"BirthYear": 1871,
"Books": {
"$id": "2",
"$values": [
{
"$id": "3",
"Title": "Du côté de chez Swann",
"Author": {
"$ref": "1"
}
}
]
}
}
- $id — identifiant unique de l'objet dans le JSON
- $ref — référence vers un objet déjà sérialisé
Lors de la désérialisation tout sera rétabli correctement (sans boucles infinies ni erreurs de pile).
Particularités et limitations
- Ce JSON est inhabituel pour le front-end : la plupart des clients JS ne comprennent pas les $id/$ref sans logique supplémentaire.
- Le JSON est plus volumineux et plus difficile à déboguer à l'œil.
- Ça fonctionne uniquement si vous activez explicitement ReferenceHandler.Preserve.
- Ne concerne pas les types valeur (il ne peut pas y avoir de cycles pour eux).
Comment désérialiser ce JSON ?
Exactement comme d'habitude, mais en réutilisant les mêmes JsonSerializerOptions :
var deserializedAuthor = JsonSerializer.Deserialize<Author>(json, options);
5. Et qu'en est-il de Newtonsoft.Json (Json.NET) ?
Historiquement Newtonsoft.Json gérait les cycles avant System.Text.Json. Il existe l'attribut [JsonObject(IsReference = true)] et des réglages globaux de sérialisation.
Attributs pour les références
[JsonObject(IsReference = true)]
public class Author
{
public string Name { get; set; }
public List<Book> Books { get; set; }
}
[JsonObject(IsReference = true)]
public class Book
{
public string Title { get; set; }
public Author Author { get; set; }
}
Puis on sérialise comme ceci :
var settings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
Formatting = Formatting.Indented
};
string json = JsonConvert.SerializeObject(author, settings);
Au final on obtient un JSON avec $id et $ref, similaire au mode ReferenceHandler.Preserve.
Résumé rapide
- Si l'échange se fait entre applications .NET — activez la sérialisation par référence (ReferenceHandler.Preserve ou PreserveReferencesHandling).
- Si les données vont vers JavaScript/autres clients — cassez les cycles : [JsonIgnore], nettoyage des références ou passage aux Id.
6. Comment éviter les erreurs et la migraine
Très souvent les débutants (et même des expérimentés) voient le sérialiseur planter à cause des cycles. Rappelez-vous : si des collections/propriétés se réfèrent mutuellement — revoyez l'architecture du modèle.
N'hésitez pas à utiliser [JsonIgnore] pour les propriétés qui ne sont pas nécessaires pour l'échange externe.
Le piège classique — la sérialisation des relations many-to-many (par exemple étudiants ↔ cours). Sans casser les cycles ou sans sérialisation par référence, ça ne fonctionne pas.
Dans les REST API on envoie souvent l'objet "dans un sens" : par exemple, le Book connaît son Author, et l'Author ne connaît que les Id des books (ou ne connaît pas du tout les books dans ce contrat).
GO TO FULL VERSION