1. Introduction
Imagine une appli pour une librairie : les objets ne sont pas que des livres, il y a aussi des auteurs avec leur bio, des éditeurs qui publient, des employés, des sections... Si la sérialisation ne gérait que des objets "plats", ton appli resterait une simple liste de contacts. Dans les projets réels les données sont presque toujours imbriquées et multiniveaux. Savoir sérialiser et désérialiser des structures hiérarchiques est une compétence qui distingue un développeur moyen d'un vrai maître de la sérialisation .NET.
On va voir comment les sérialiseurs modernes en C# (avec l'exemple de System.Text.Json) permettent de persister non seulement des arbres d'objets, mais de véritables "jungles". Et aussi comment concevoir correctement les classes pour la sérialisation, pour éviter de devoir extraire manuellement les données ensuite.
2. Modélisation des structures hiérarchiques
Étendons notre modèle. Dans les exemples précédents on avait Book, Author et la classe Library qui contient une liste de livres. Maintenant ajoutons un niveau : chaque auteur aura une liste de livres qu'il a écrits (oui, ça duplique l'info — on verra plus tard ce que ça implique), et la bibliothèque aura aussi une liste d'employés Employee.
Schéma des classes
Voici à quoi ressemblera globalement notre structure d'objets :
classDiagram
class Library {
List~Book~ Books
List~Employee~ Employees
string Name
}
class Book {
string Title
Author Author
int Year
}
class Author {
string Name
int BirthYear
List~Book~ Books
}
class Employee {
string Name
string Position
}
Library "1" -- "many" Book
Library "1" -- "many" Employee
Book "1" -- "1" Author
Author "1" -- "many" Book
Cette approche nous permet de construire un vrai système de bibliothèque. Oui, il y a un risque de références "bouclées" (par exemple, l'auteur a des livres, le livre a un auteur — sérialisation infinie !). On en reparle plus loin.
3. Exemple d'implémentation des classes
On décrit d'abord les classes C# pour notre modèle. J'ajoute aussi des commentaires :
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
public class Library
{
public string Name { get; set; }
public List<Book> Books { get; set; } = new();
public List<Employee> Employees { get; set; } = new();
}
public class Book
{
public string Title { get; set; }
public int Year { get; set; }
public Author Author { get; set; }
}
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
// IMPORTANT ! Ce champ est potentiellement "dangereux" : il peut créer une référence cyclique.
// Mais pour la démo on le garde.
public List<Book> Books { get; set; } = new();
}
public class Employee
{
public string Name { get; set; }
public string Position { get; set; }
}
4. Construction de la structure : on crée la bibliothèque
Créons maintenant une bibliothèque avec des livres, des auteurs et des employés pour tester la sérialisation.
// On crée les auteurs
var author1 = new Author { Name = "William Golding", BirthYear = 1911 };
var author2 = new Author { Name = "John Updike", BirthYear = 1932 };
var author3 = new Author { Name = "J. D. Salinger", BirthYear = 1919 };
// On crée les livres
var book1 = new Book { Title = "Sa Majesté des mouches", Year = 1954, Author = author1 };
var book2 = new Book { Title = "Le Centaure", Year = 1963, Author = author2 };
var book3 = new Book { Title = "L'Attrape-cœurs", Year = 1951, Author = author3 };
// On ajoute les livres aux auteurs
author1.Books.Add(book1);
author2.Books.Add(book2);
author3.Books.Add(book3);
// On crée les employés
var emp1 = new Employee { Name = "Ivan Ivanov", Position = "Bibliothécaire" };
var emp2 = new Employee { Name = "Sergey Sergeev", Position = "Directeur" };
// On compose la bibliothèque
var library = new Library
{
Name = "Bibliothèque municipale",
Books = new List<Book> { book1, book2, book3 },
Employees = new List<Employee> { emp1, emp2 }
};
Évidemment, dans du code réel vous automatiseriez la création des objets et leurs relations pour ne pas gérer manuellement chaque livre/auteur. Mais pour l'exemple, ça suffit.
5. Sérialisation en JSON
On utilise l'approche classique avec System.Text.Json :
using System.Text.Json;
// On sérialise la bibliothèque en JSON
var options = new JsonSerializerOptions
{
WriteIndented = true, // jolis indentations
ReferenceHandler = ReferenceHandler.IgnoreCycles // on évite les boucles !
};
string json = JsonSerializer.Serialize(library, options);
// Affichons le résultat
Console.WriteLine(json);
Point intéressant : sans l'option spéciale ReferenceHandler.IgnoreCycles, la sérialisation bouclerait — l'auteur contient une liste de livres, le livre contient l'auteur, et le sérialiseur tournerait en boucle jusqu'à lever une exception. L'option IgnoreCycles règle ça : si le sérialiseur voit qu'un objet a déjà été sérialisé plus haut dans l'arborescence, il écrit simplement null à la place de la sérialisation répétée.
À quoi ressemble le JSON ?
{
"Name": "Bibliothèque municipale",
"Books": [
{
"Title": "Le Centaure",
"Year": 1963,
"Author": {
"Name": "John Updike",
"BirthYear": 1932,
"Books": [
{
"Title": "Le Centaure",
"Year": 1963,
"Author": null
},
{
"Title": "Les sorcières d'Eastwick",
"Year": 1984,
"Author": null
}
]
}
},
{
"Title": "Les sorcières d'Eastwick",
"Year": 1984,
"Author": {
"Name": "John Updike",
"BirthYear": 1932,
"Books": [
{
"Title": "Le Centaure",
"Year": 1963,
"Author": null
},
{
"Title": "Les sorcières d'Eastwick",
"Year": 1984,
"Author": null
}
]
}
}
],
"Employees": [
{
"Name": "Ivan Ivanov",
"Position": "Bibliothécaire"
},
{
"Name": "Sergey Sergeev",
"Position": "Directeur"
}
]
}
Remarque : dans le tableau Books l'objet author des livres imbriqués n'a plus d'info sur son propre author — à la place il y a Author : null. C'est le moyen de casser la boucle pendant la sérialisation.
6. Désérialisation vers des objets
Maintenant on désérialise :
// On restaure l'objet depuis le JSON
var libraryCopy = JsonSerializer.Deserialize<Library>(json, options);
Console.WriteLine(libraryCopy.Name); // "Bibliothèque municipale"
Console.WriteLine($"Livres : {libraryCopy.Books.Count}");
Console.WriteLine($"Employés : {libraryCopy.Employees.Count}");
Attention ! La restauration des références cycliques n'est pas parfaite : les livres imbriqués dans la liste de l'auteur auront Author égal à null, parce que le sérialiseur a tronqué la chaîne pour éviter l'imbrication infinie.
Point important : La sérialisation de relations mutuelles complexes (par ex. parent — enfants — parent) avec le sérialiseur standard exige toujours un compromis : soit on perd certaines liaisons, soit on les restaure manuellement après la désérialisation.
7. Références cycliques (imbriqué vs cycles)
Si votre structure contient des cas où un objet se référence via une chaîne d'objets (référence cyclique), les sérialiseurs standard comme System.Text.Json et Newtonsoft.Json, surtout en mode typé strict, réagissent différemment. Avant l'option ReferenceHandler.IgnoreCycles, la sérialisation levait une exception du type "ReferenceLoopHandling detected". Maintenant il met simplement null à la place de la référence répétée.
Quel est le piège ?
Avantage : ton code ne plante pas avec une erreur.
Inconvénient : après désérialisation il faut parfois reconstruire manuellement des liaisons. Par exemple, si un graphe d'utilisateurs se référence mutuellement (employé et son manager), après désérialisation certaines références peuvent être nulles.
8. Comment concevoir des structures complexes pour la sérialisation
Si tu sais à l'avance que tes objets vont former des cycles, ou si tu veux que toutes les liaisons soient intactes après restauration, il vaut mieux stocker des identifiants plutôt que des objets complets.
Exemple : stocker des identifiants au lieu de références
Modifions la classe Book pour que la référence à l'auteur soit via un identifiant :
public class Book
{
public string Title { get; set; }
public int Year { get; set; }
public int AuthorId { get; set; }
}
Au lieu d'une liste de livres dans Author, on garde une liste d'identifiants de livres. Pour reconstruire les relations après désérialisation il faudra faire un "matching" par id, mais il n'y aura plus de cycles dangereux.
Pourquoi c'est important en vrai ?
- Les bases de données utilisent presque toujours des identifiants, c'est plus simple pour l'export/import.
- Les REST API échangent souvent des id plutôt que des structures imbriquées très complexes.
9. Collections imbriquées : sérialiser des arbres
Cas fréquent — hiérarchies à profondeur arbitraire : arbre de dossiers, structure de menu, catalogue avec sous-catégories.
Exemple de classe pour un "arbre" :
public class Folder
{
public string Name { get; set; }
public List<Folder> Children { get; set; } = new();
}
On crée un arbre :
var root = new Folder
{
Name = "Root",
Children = new List<Folder>
{
new Folder { Name = "Sub1", Children = { new Folder { Name = "Sub1-1" } } },
new Folder { Name = "Sub2" }
}
};
On sérialise et affiche :
string jsonTree = JsonSerializer.Serialize(root, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonTree);
Le JSON pour cet arbre montre visuellement l'arborescence avec des tableaux imbriqués.
10. Particularités de la sérialisation des tableaux et listes
Si une propriété est un tableau (T[]) ou une collection (List<T>), la sérialisation la transforme en un tableau JSON classique.
public class Shop
{
public string Name { get; set; }
public string[] Departments { get; set; }
}
var shop = new Shop
{
Name = "Supermarché",
Departments = new[] { "Légumes", "Fruits", "Viande" }
};
string jsonShop = JsonSerializer.Serialize(shop, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonShop);
Le JSON ressemblera à :
{
"Name": "Supermarché",
"Departments": [
"Légumes",
"Fruits",
"Viande"
]
}
11. Influence des attributs sur les objets imbriqués
Si tu utilises [JsonIgnore] sur des propriétés dans des objets imbriqués, elles seront exclues du JSON final quel que soit le niveau d'imbrication.
public class SecretBook : Book
{
[JsonIgnore]
public string SecretCode { get; set; }
}
C'est souvent utilisé pour protéger des données privées : si tu ne veux pas sérialiser certaines informations internes d'un objet, ajoute l'attribut — et c'est réglé.
12. Conseils pratiques
- Aux entretiens on demande souvent : «Comment sérialiser un arbre (Tree) ?» et «Que faire avec les références cycliques ?». Prépare des exemples avec ReferenceHandler.IgnoreCycles et le stockage par identifiants.
- En prod on sérialise des commandes, factures, utilisateurs, catalogues produits, rapports complexes. L'imbrication est partout.
- Si tu travailles avec des graphes ou des arbres, évite les cycles ou utilise des id.
- Quand tu consommes un API externe, définis à l'avance le format des structures imbriquées pour éviter les surprises au parsing JSON.
GO TO FULL VERSION