CodeGym /Cours /C# SELF /Travail avec des objets imbriqués et des collections

Travail avec des objets imbriqués et des collections

C# SELF
Niveau 45, Leçon 1
Disponible

1. Introduction

La sérialisation d'objets simples, c'est comme envoyer une carte postale : facile, rien ne se perd. Mais souvent nos objets deviennent des "albums de famille" : ils contiennent des objets imbriqués, des collections, et parfois des collections de collections.

Imagine que notre utilisateur n'est pas juste une Person avec un nom et un âge, mais qu'il a par exemple un ensemble de contacts (Contact), plusieurs adresses perso/pro, et peut-être un champ de type List<Pet> si la personne aime les animaux. Tout ça, ce sont des objets imbriqués et des collections.

Comment les serializers .NET gèrent-ils ça ? Quelles subtilités faut-il garder en tête quand on sérialise des arbres d'objets complexes ? Est-ce que tout peut casser si des collections contiennent d'autres collections ? Aujourd'hui on va voir à quoi s'attendre — et quoi faire dans les cas délicats.

Que signifie "imbriqué" en termes .NET

Un objet imbriqué, c'est simplement un autre objet comme propriété ou champ à l'intérieur de votre objet principal. Par exemple, voici notre modèle utilisateur étendu :

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<Contact> Contacts { get; set; }         // Collection d'objets imbriqués
    public Address? HomeAddress { get; set; }           // Un objet imbriqué (nullable)
}
public class Contact
{
    public string Type { get; set; }                    // Par exemple, Email ou Phone
    public string Value { get; set; }
}
public class Address
{
    public string City { get; set; }
    public string Street { get; set; }
}

Remarque — une personne peut avoir plusieurs contacts, et l'adresse n'est qu'une seule (et peut être null). C'est classique dans beaucoup de modèles.

2. Sérialisation d'objets et de collections avec System.Text.Json

Exemple simple : sérialisons et désérialisons une collection

Regardons comment ça se passe en pratique :

using System.Text.Json;

var person = new Person
{
    Name = "Anna",
    Age = 28,
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "anna@example.com" },
        new Contact { Type = "Phone", Value = "+1234567890" }
    },
    HomeAddress = new Address { City = "Berlin", Street = "Alexanderplatz, 1" }
};

string json = JsonSerializer.Serialize(person, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

Le résultat sera à peu près le suivant :

{
  "Name": "Anna",
  "Age": 28,
  "Contacts": [
    {
      "Type": "Email",
      "Value": "anna@example.com"
    },
    {
      "Type": "Phone",
      "Value": "+1234567890"
    }
  ],
  "HomeAddress": {
    "City": "Berlin",
    "Street": "Alexanderplatz, 1"
  }
}

Le serializer comprend très bien l'imbriquement. Si une propriété est un autre objet, il le sérialise "imbriqué". Si c'est une liste, il la sérialise comme un tableau.

Fait : cette sérialisation fonctionne automatiquement pour n'importe quelle collection et tout objet imbriqué, à condition qu'ils soient publics et qu'ils aient un getter et setter public (get/set).

Désérialisation : ça marche "out of the box"

string jsonInput = /* JSON qu'on vient de recevoir */;
Person deserializedPerson = JsonSerializer.Deserialize<Person>(jsonInput);
Console.WriteLine(deserializedPerson.Name); // "Anna"
Console.WriteLine(deserializedPerson.Contacts[0].Type); // "Email"

Tout marche — les contacts passent du tableau JSON vers une List<Contact>, HomeAddress redevient un objet Address.

3. Comment sont sérialisées les collections : List, tableaux, Dictionary

Souvent dans votre modèle il y a non seulement des listes (List<T>), mais aussi des dictionnaires (Dictionary<TKey, TValue>) ou des tableaux multidimensionnels.

Exemple avec un tableau

public class Team
{
    public string Name { get; set; }
    public Person[] Members { get; set; }
}

var team = new Team
{
    Name = "Développeurs",
    Members = new[]
    {
        new Person { Name = "Alexey", Age = 31 },
        new Person { Name = "Ekaterina", Age = 27 }
    }
};
string json = JsonSerializer.Serialize(team, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON :

{
  "Name": "Développeurs",
  "Members": [
    {
      "Name": "Alexey",
      "Age": 31,
      "Contacts": null,
      "HomeAddress": null
    },
    {
      "Name": "Ekaterina",
      "Age": 27,
      "Contacts": null,
      "HomeAddress": null
    }
  ]
}

La sérialisation d'une collection ressemble à l'emballage d'une pile de cartes identiques : chaque élément est une "carte" séparée.

Exemple avec Dictionary

public class Phonebook
{
    public Dictionary<string, string> Phones { get; set; }
}

var phonebook = new Phonebook
{
    Phones = new Dictionary<string, string>
    {
        { "Andrey", "+12998887766" },
        { "Maria", "+12882223344" }
    }
};

string json = JsonSerializer.Serialize(phonebook, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON :

{
  "Phones": {
    "Andrey": "+12998887766",
    "Maria": "+12882223344"
  }
}

En JSON, un dictionnaire devient un objet avec des clés dynamiques.

4. Collections imbriquées (List dans List, tableaux de tableaux)

.NET sait aussi sérialiser ces "matriochkas" :

public class Zoo
{
    public List<List<Animal>> AnimalGroups { get; set; }
}
public class Animal { public string Name { get; set; } }

var zoo = new Zoo
{
    AnimalGroups = new List<List<Animal>>
    {
        new List<Animal> { new Animal { Name = "Lion" }, new Animal { Name = "Tigre" } },
        new List<Animal> { new Animal { Name = "Ours" } }
    }
};

string json = JsonSerializer.Serialize(zoo, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON :

{
  "AnimalGroups": [
    [ { "Name": "Lion" }, { "Name": "Tigre" } ],
    [ { "Name": "Ours" } ]
  ]
}

Idem pour la désérialisation dans l'autre sens.

5. Particularités de la sérialisation d'objets imbriqués avec XmlSerializer

Exemple de sérialisation d'un objet imbriqué

using System.Xml.Serialization;
using System.IO;

var person = new Person
{
    Name = "Ivan",
    Age = 35,
    HomeAddress = new Address { City = "Bonn", Street = "Beethovenstr., 100" },
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "ivan@domain.de" }
    }
};

var serializer = new XmlSerializer(typeof(Person));
using var writer = new StringWriter();
serializer.Serialize(writer, person);
Console.WriteLine(writer.ToString());

Résultat :

<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>Ivan</Name>
  <Age>35</Age>
  <Contacts>
    <Contact>
      <Type>Email</Type>
      <Value>ivan@domain.de</Value>
    </Contact>
  </Contacts>
  <HomeAddress>
    <City>Bonn</City>
    <Street>Beethovenstr., 100</Street>
  </HomeAddress>
</Person>

Collections et XmlSerializer

Par défaut, XmlSerializer sérialise une collection comme une balise "Contacts", à l'intérieur de laquelle chaque élément devient une balise "Contact". Pour ça, le type de collection doit être public et ses éléments des types sérialisables.

Point important. Si la collection est vide, le XML inclura quand même une balise vide :

<Contacts />

Collections supportées. XmlSerializer supporte List<T>, les tableaux T[], les collections qui implémentent ICollection<T>. Il ne supporte pas, par exemple, Dictionary<TKey, TValue> ! Pour sérialiser des dictionnaires il faudra envelopper le dictionnaire dans une liste de paires ou utiliser d'autres techniques / libs.

6. Support des objets et collections dans Newtonsoft.Json

Newtonsoft.Json offre un peu plus de flexibilité pour l'imbriqué. Dans 99% des cas tout fonctionne pareil que System.Text.Json, mais il y a des avantages : on peut sérialiser des champs privés (si on le spécifie), des dictionnaires avec des clés plus complexes, des types dynamiques, et même gérer des références cycliques (avec des réglages spécifiques).

Exemple

using Newtonsoft.Json;

var person = new Person
{
    Name = "Pavel",
    Age = 40,
    Contacts = new List<Contact>
    {
        new Contact { Type = "SMS", Value = "+10000000013" }
    }
};
string json = JsonConvert.SerializeObject(person, Formatting.Indented);
Console.WriteLine(json);

Différence. Si un objet a, par exemple, des champs privés, on peut les sérialiser via des réglages de ContractResolver. Mais pour les collections et objets imbriqués de base tout marche "out of the box".

7. Erreurs typiques, pièges et subtilités

Erreur 1 : Objets imbriqués = null.
Si dans l'objet que vous sérialisez certaines propriétés ne sont pas renseignées (null), alors en JSON ou XML elles peuvent être absentes, ou bien inscrites comme null. Par exemple, si HomeAddress d'une Person est null, en JSON :

"HomeAddress": null

En XML, par défaut, il n'y aura pas cette balise — on peut l'ajouter via des réglages du serializer ([XmlElement(IsNullable=true)]).

Erreur 2 : Sérialiser des propriétés/champs privés.
Par défaut la plupart des serializers (JSON et XML) ne travaillent qu'avec des propriétés publiques. Si tu veux sérialiser des champs privés ou protected, il faudra les rendre publics ou configurer le serializer (par exemple via ContractResolver dans Newtonsoft.Json).

Erreur 3 : Classes sans constructeur par défaut.
Pour la sérialisation XML (et souvent pour JSON), la classe doit avoir un constructeur public sans paramètres. Si ce constructeur manque, tu auras une exception à la désérialisation.

Erreur 4 : Sérialisation de dictionnaires en XML.
XmlSerializer ne supporte pas nativement Dictionary<TKey, TValue>. Ça surprend souvent. La solution peut être d'envelopper le dictionnaire dans une liste de paires ou d'utiliser un autre serializer.

Erreur 5 : Références cycliques.
Si un objet référence un autre objet qui à son tour référence le premier (par ex. parent <-> enfant), la sérialisation "en boucle" peut provoquer un débordement de pile (StackOverflowException) ou une exception de référence cyclique. En JSON tu peux contourner ça via des settings, mais souvent il est préférable de revoir l'architecture des objets.

2
Mission
C# SELF, niveau 45, leçon 1
Bloqué
Sérialisation et désérialisation d'une structure complexe
Sérialisation et désérialisation d'une structure complexe
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION