1. Introduction
Dans ce cours on fait le pas de "donne-moi juste sauvegarder la liste" vers une sérialisation flexible, précise et performante avec System.Text.Json. Dans les projets .NET modernes, JSON est le standard de facto pour l'échange de données. Les appels de base Serialize/Deserialize sont simples, mais les cas réels demandent des réglages fins : ignorer/renommer des champs, gérer les formats de date, se protéger contre les cycles, travailler avec de gros volumes, créer des convertisseurs personnalisés, etc.
On verra non seulement les méthodes du JsonSerializer, mais aussi les paramètres via JsonSerializerOptions, les attributs, le travail avec Stream, la gestion de la mémoire et l'injection de règles de sérialisation via JsonConverter.
Brève histoire et positionnement de System.Text.Json
Pendant longtemps, Newtonsoft.Json (Json.NET) dominait dans .NET — flexible et mature, mais pas toujours le plus rapide ni le plus léger côté dépendances. Depuis .NET Core 3.0 il existe le serializer intégré System.Text.Json : haute performance, dépendances minimales (inclus dans la plateforme), intégration serrée avec ASP.NET Core et évolution constante avec les releases de .NET.
2. Classes et méthodes principales
L'élément de base est la classe statique JsonSerializer, qui offre deux directions :
- Sérialisation : objet → chaîne JSON (Serialize)
- Désérialisation : chaîne JSON → objet du type voulu (Deserialize)
Exemple de sérialisation d'un objet simple
using System.Text.Json;
var person = new Person { Name = "Ivan", Age = 30 };
string jsonString = JsonSerializer.Serialize(person);
Console.WriteLine(jsonString); // {"Name":"Ivan","Age":30}
Exemple de désérialisation
var json = "{\"Name\":\"Anna\",\"Age\":22}";
var anna = JsonSerializer.Deserialize<Person>(json);
Console.WriteLine(anna.Name); // Anna
Remarque : le type Person a déjà été implémenté dans les leçons précédentes — on l'utilise aussi ici.
3. Contrôler la sérialisation : JsonSerializerOptions
Dans les projets réels on a presque toujours besoin de réglages : noms de propriétés en camelCase, formats de dates, gestion des cycles, valeurs par défaut, etc. Tout cela se configure via JsonSerializerOptions.
Exemple de configuration
var options = new JsonSerializerOptions
{
WriteIndented = true, // Formater joliment le JSON (ajoute des espaces et des retours)
PropertyNameCaseInsensitive = true, // Ignorer la casse des noms de propriétés lors de la désérialisation
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // camelCase pour les propriétés (plutôt que PascalCase)
};
string json = JsonSerializer.Serialize(person, options);
/*
{
"name": "Ivan",
"age": 30
}
*/
Pourquoi c'est important ? La plupart des frameworks frontend attendent du camelCase, pas le PascalCase de .NET.
4. Attributs : System.Text.Json.Serialization
Parfois il est plus pratique de contrôler la sérialisation directement depuis le modèle avec des attributs. On les ajoute aux champs/propriétés pour influencer les noms, l'inclusion/exclusion et le traitement des valeurs.
Attributs principaux
| Attribut | Ce que ça fait |
|---|---|
|
Exclut la propriété de la sérialisation/désérialisation |
|
Utilise un autre nom dans le JSON |
|
Inclut une propriété/champ non-public dans la sérialisation |
|
Contrôle le traitement des valeurs numériques |
Exemple : contrôle des propriétés via des attributs
using System.Text.Json.Serialization;
public class Person
{
[JsonPropertyName("full_name")]
public string Name { get; set; }
[JsonIgnore]
public int SecretCode { get; set; }
public int Age { get; set; }
}
var person = new Person { Name = "Piotr", Age = 45, SecretCode = 123 };
string json = JsonSerializer.Serialize(person);
// {"full_name":"Piotr","Age":45}
Attention : SecretCode n'est pas présent dans le JSON, et Name a été sérialisé comme "full_name".
5. Sérialisation de collections et d'objets imbriqués
Les collections — c'est simple
var numbers = new List<int> { 1, 2, 3 };
string json = JsonSerializer.Serialize(numbers); // [1,2,3]
var people = new List<Person> {
new Person { Name = "Anna", Age = 20 },
new Person { Name = "Maxim", Age = 40 }
};
string jsonList = JsonSerializer.Serialize(people);
// [{"Name":"Anna","Age":20},{"Name":"Maxim","Age":40}]
Structures imbriquées
public class Group
{
public string Name { get; set; }
public List<Person> Members { get; set; }
}
var group = new Group
{
Name = "Developpeurs",
Members = new List<Person>
{
new Person { Name = "Sacha", Age = 23 },
new Person { Name = "Macha", Age = 28 }
}
};
string jsonGroup = JsonSerializer.Serialize(group, options);
/*
{
"name": "Developpeurs",
"members": [
{ "name": "Sacha", "age": 23 },
{ "name": "Macha", "age": 28 }
]
}
*/
6. Désérialisation : points importants
var json = "[{\"Name\":\"Ivan\",\"Age\":21}]";
var list = JsonSerializer.Deserialize<List<Person>>(json);
Console.WriteLine(list[0].Name); // Ivan
Scénario fréquent : si un champ manque dans le JSON, la propriété correspondante recevra la valeur par défaut. Les champs supplémentaires dans le JSON qui n'existent pas dans le modèle sont ignorés. Mais si les types ne correspondent pas (par ex. une string au lieu d'un nombre) — la désérialisation lèvera une exception.
7. Gestion des dates, heures, formats et valeurs numériques
public class Meeting
{
public string Topic { get; set; }
public DateTime Time { get; set; }
}
var meeting = new Meeting { Topic = "Reunion", Time = DateTime.Now };
string json = JsonSerializer.Serialize(meeting);
// {"Topic":"Reunion","Time":"2024-06-06T20:30:00.0000000+03:00"}
Par défaut DateTime est sérialisé en ISO 8601. Besoin d'un autre format (par ex. seulement la date) ? Utilisez une propriété séparée ou un convertisseur personnalisé (voir plus bas).
FAQ : Pour sérialiser des nombres en tant que chaînes (par ex. téléphones ou gros ID), utilisez l'attribut [JsonNumberHandling(JsonNumberHandling.WriteAsString)].
8. Streams et travail avec des fichiers
On peut travailler non seulement avec des chaînes, mais aussi avec des Stream — important pour les grosses données (fichiers, réseau).
Exemple d'écriture dans un fichier
using var fs = File.Create("person.json");
JsonSerializer.Serialize(fs, person);
// N'oubliez pas d'appeler fs.Flush() ou d'utiliser using !
Exemple de lecture depuis un fichier
using var fs = File.OpenRead("person.json");
var restored = JsonSerializer.Deserialize<Person>(fs);
Avec les streams il existe des méthodes asynchrones SerializeAsync/DeserializeAsync — utile pour des services à haute charge.
9. Convertisseurs personnalisés
Si les règles standard ne conviennent pas (formats de date/nombre non standard, valeurs complexes, structures propres) — on écrit un JsonConverter.
Exemple : date seulement au format "dd.MM.yyyy"
public class CustomDateConverter : JsonConverter<DateTime>
{
public override DateTime Read(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.ParseExact(reader.GetString(), "dd.MM.yyyy", null);
}
public override void Write(
Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("dd.MM.yyyy"));
}
}
var options = new JsonSerializerOptions();
options.Converters.Add(new CustomDateConverter());
var dt = new DateTime(2024, 6, 1);
string json = JsonSerializer.Serialize(dt, options); // "01.06.2024"
Les convertisseurs personnalisés sont utiles pour sérialiser des coordonnées, vecteurs, couleurs, dates et devises non standard, différents formats d'ID, etc.
10. Nuances utiles
Gestion des références cycliques et des hiérarchies profondes
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve, // Préserve tous les objets avec $id/$ref
WriteIndented = true
};
Important : le JSON contiendra des propriétés utilitaires $id et $ref. Pour l'échange avec des systèmes externes qui ne les comprennent pas, ça peut ne pas convenir.
Différences entre System.Text.Json et Newtonsoft.Json
System.Text.Json est déjà très puissant, mais ne couvre pas encore tous les scénarios de Newtonsoft.Json (constructeurs privés, objets dynamiques complexes, etc.). Pour la majorité des tâches standard, on recommande le serializer intégré — il est plus rapide et sans dépendances superflues.
Travail interactif avec JSON : API DOM
Quand il faut "parcourir" le JSON sans modèle complet, utilisez JsonDocument et JsonElement.
using var doc = JsonDocument.Parse(jsonString);
JsonElement root = doc.RootElement;
if (root.TryGetProperty("Name", out var nameProperty))
{
Console.WriteLine(nameProperty.GetString());
}
11. Options utiles et leurs effets
| Propriété | Valeur/Objectif |
|---|---|
|
true — formatage avec indentations |
|
true — ignorer la casse des noms de propriétés lors de la désérialisation |
|
|
|
Règles pour ignorer null/valeurs par défaut |
|
, |
|
true — autoriser la virgule finale dans un tableau |
|
Conversion des nombres en chaînes/retour (et autres) |
|
Liste de convertisseurs personnalisés |
12. Erreurs fréquentes et conseils pratiques
Erreur n°1 : mauvais type lors de la désérialisation. Si vous avez sérialisé une liste, désérialisez-la en liste : List<T>, pas en objet unique.
Erreur n°2 : mauvaise casse des noms de propriétés. Sans réglage la casse peut empêcher la correspondance. Utilisez PropertyNameCaseInsensitive ou configurez PropertyNamingPolicy.
Erreur n°3 : gestion incorrecte des dates. Par défaut le format est ISO 8601. Besoin d'autre chose — créez et appliquez un convertisseur (JsonConverter<DateTime>).
Erreur n°4 : attente de sérialisation de champs privés/statiques. Par défaut seules les propriétés publiques sont prises. Pour des cas non standards, utilisez les attributs appropriés (par ex. [JsonInclude]).
Erreur n°5 : méconnaissance des valeurs par défaut. Champ absent dans le JSON → valeur par défaut pour la propriété. Tenez-en compte dans la logique.
Erreur n°6 : mauvaise gestion des streams. Fermez les ressources avec using ou await using pour éviter fuites et blocages.
GO TO FULL VERSION