Menu

JSON en C# : sérialiser et désérialiser avec System.Text.Json

Comment travailler avec du JSON en C# avec System.Text.Json : JsonSerializer.Serialize et Deserialize, sortie en camelCase et indentée, attributs comme JsonPropertyName et JsonIgnore, enums sous forme de chaînes, lire du JSON sans classe avec JsonDocument et JsonNode, erreurs, et comparaison avec Newtonsoft.Json.

Le JSON est la façon dont la plupart des programmes C# dialoguent avec les API web, stockent leurs réglages et échangent des données. Le .NET moderne le lit et l'écrit avec System.Text.Json, qui fait partie du runtime depuis .NET Core 3.0 : aucun package NuGet n'est nécessaire. Son point d'entrée principal est la classe statique JsonSerializer, qui transforme des objets en chaînes JSON et inversement.

System.Text.Json est intégré à .NET Core 3.0 et à toutes les versions suivantes (de .NET 5 à .NET 10). Les projets sur .NET Framework 4.6.2 ou plus, ou sur .NET Standard 2.0, peuvent aussi l'utiliser en installant le package NuGet System.Text.Json. Les exemples de cette page sont présentés en code simple, avec la sortie en commentaires. Ajoutez ces directives using pour les exécuter dans un projet .NET :

using System.Text.Json;
using System.Text.Json.Serialization;

Sérialiser : d'un objet vers JSON

JsonSerializer.Serialize écrit chaque propriété publique d'un objet, avec les noms de propriétés tels quels :

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public List<string> Tags { get; set; } = new List<string>();
    public bool InStock { get; set; }
}

var lamp = new Product { Name = "Desk lamp", Price = 34.90m, Tags = { "home", "light" }, InStock = true };

string json = JsonSerializer.Serialize(lamp);
Console.WriteLine(json);
// {"Name":"Desk lamp","Price":34.90,"Tags":["home","light"],"InStock":true}

Les collections deviennent des tableaux, les dictionnaires à clés chaînes deviennent des objets ({"apples":3,"pears":5}), null reste null, et DateTime devient une chaîne ISO 8601 ("2026-03-01T14:30:00"). Deux comportements par défaut piègent les développeurs :

  • Les champs sont ignorés. Seules les propriétés sont sérialisées. Une classe avec public int X; se sérialise en {}, sauf si vous définissez IncludeFields = true dans les options ou transformez le champ en propriété.
  • Les enums sont des nombres. OrderStatus.Shipped s'écrit 1 (la valeur sous-jacente de l'enum). Ajoutez JsonStringEnumConverter (voir plus bas) pour écrire "Shipped".

Désérialiser : de JSON vers un objet

JsonSerializer.Deserialize<T> crée un T et définit ses propriétés à partir du JSON :

string json = "{\"Name\":\"Desk lamp\",\"Price\":34.90,\"Tags\":[\"home\",\"light\"]}";
Product p = JsonSerializer.Deserialize<Product>(json);
Console.WriteLine($"{p.Name} {p.Price} {p.Tags.Count}");   // Desk lamp 34.90 2

var many = JsonSerializer.Deserialize<List<Product>>("[{\"Name\":\"A\",\"Price\":1},{\"Name\":\"B\",\"Price\":2.5}]");
Console.WriteLine(many.Count);                               // 2

Les propriétés JSON sans propriété C# correspondante sont ignorées, et les propriétés C# sans JSON correspondant gardent leurs valeurs par défaut. Ni l'un ni l'autre n'est une erreur par défaut.

La règle qui piège presque tout le monde : la correspondance des noms est sensible à la casse. La plupart des API web envoient du camelCase, et le camelCase ne correspond pas aux propriétés en PascalCase :

string fromApi = "{\"name\":\"Mug\",\"price\":8.5}";

var a = JsonSerializer.Deserialize<Product>(fromApi);
Console.WriteLine($"[{a.Name}] {a.Price}");    // [] 0: nothing matched, no error

var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var b = JsonSerializer.Deserialize<Product>(fromApi, options);
Console.WriteLine($"[{b.Name}] {b.Price}");    // [Mug] 8.5

new JsonSerializerOptions(JsonSerializerDefaults.Web) donne les réglages qu'utilise ASP.NET Core : lecture insensible à la casse, écriture en camelCase, et nombres acceptés sous forme de chaînes entre guillemets.

La désérialisation a besoin d'un moyen de définir chaque valeur : un setter public, un accesseur init, ou un constructeur dont les noms de paramètres correspondent aux propriétés. Cette dernière règle explique pourquoi les records fonctionnent directement :

public record Point(int X, int Y);

Point pt = JsonSerializer.Deserialize<Point>("{\"X\":1,\"Y\":2}");
Console.WriteLine(pt);   // Point { X = 1, Y = 2 }

Options : camelCase et sortie indentée

JsonSerializerOptions contrôle le nommage, le formatage et plus encore. Créez une instance et réutilisez-la : le sérialiseur met en cache des métadonnées par instance d'options, donc construire de nouvelles options à chaque appel est sensiblement plus lent.

private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true,
};

Console.WriteLine(JsonSerializer.Serialize(lamp, Options));
// {
//   "name": "Desk lamp",
//   "price": 34.90,
//   "tags": [
//     "home",
//     "light"
//   ],
//   "inStock": true
// }

D'autres options utiles : DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull pour omettre les propriétés null, IncludeFields = true, NumberHandling pour accepter des nombres écrits sous forme de chaînes, et ReadCommentHandling = JsonCommentHandling.Skip plus AllowTrailingCommas = true pour des fichiers de configuration édités à la main. .NET 8 a ajouté JsonNamingPolicy.SnakeCaseLower pour les API qui utilisent le snake_case.

Attributs : renommer, ignorer, enums en chaînes

Les attributs placés sur la classe contrôlent une propriété à la fois et l'emportent sur les options :

public enum OrderStatus { Pending, Shipped }

public class Order
{
    [JsonPropertyName("order_id")]
    public int Id { get; set; }

    public string Customer { get; set; }

    [JsonIgnore]
    public string InternalNote { get; set; }                  // never written or read

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Coupon { get; set; }                        // left out when null

    [JsonConverter(typeof(JsonStringEnumConverter))]
    public OrderStatus Status { get; set; }

    public DateTime PlacedAt { get; set; }
}

var order = new Order
{
    Id = 1042, Customer = "Ana", InternalNote = "vip",
    Status = OrderStatus.Shipped, PlacedAt = new DateTime(2026, 3, 1, 14, 30, 0),
};
Console.WriteLine(JsonSerializer.Serialize(order));
// {"order_id":1042,"Customer":"Ana","Status":"Shipped","PlacedAt":"2026-03-01T14:30:00"}

Pour écrire chaque enum sous forme de chaîne sans marquer chaque propriété, ajoutez le convertisseur aux options : options.Converters.Add(new JsonStringEnumConverter());.

Lire du JSON sans classe : JsonDocument et JsonNode

Quand vous n'avez besoin que de quelques valeurs d'une grosse réponse, ou que sa forme varie, passez-vous de classe. JsonDocument analyse le JSON en un arbre en lecture seule de valeurs JsonElement :

string weather = "{\"city\":\"Lisbon\",\"current\":{\"temp\":21.5,\"conditions\":[\"sunny\",\"windy\"]},\"alerts\":null}";

using (JsonDocument doc = JsonDocument.Parse(weather))
{
    JsonElement root = doc.RootElement;
    Console.WriteLine(root.GetProperty("city").GetString());                        // Lisbon
    Console.WriteLine(root.GetProperty("current").GetProperty("temp").GetDecimal()); // 21.5

    foreach (JsonElement c in root.GetProperty("current").GetProperty("conditions").EnumerateArray())
        Console.WriteLine(c.GetString());                                            // sunny, windy

    Console.WriteLine(root.TryGetProperty("humidity", out _));                       // False
    Console.WriteLine(root.GetProperty("alerts").ValueKind);                         // Null
}

GetProperty lève KeyNotFoundException pour un nom absent, donc utilisez TryGetProperty pour les champs facultatifs. JsonDocument emprunte de la mémoire à un pool, c'est pourquoi on le libère avec using.

Pour modifier du JSON, utilisez JsonNode de System.Text.Json.Nodes (.NET 6 et plus), qui donne un arbre modifiable avec des indexeurs :

JsonNode node = JsonNode.Parse(weather);
Console.WriteLine((string)node["city"]);   // Lisbon

node["current"]["temp"] = 23;
node["updated"] = true;
Console.WriteLine(node.ToJsonString());
// {"city":"Lisbon","current":{"temp":23,"conditions":["sunny","windy"]},"alerts":null,"updated":true}

Fichiers et flux

Du JSON sur disque est une chaîne dans un fichier, donc les méthodes de fichiers se combinent directement avec le sérialiseur :

File.WriteAllText("settings.json", JsonSerializer.Serialize(settings, Options));
var loaded = JsonSerializer.Deserialize<Settings>(File.ReadAllText("settings.json"), Options);
// Same options both ways: camelCase names written with Options would not match on a default read.

Pour les gros fichiers et les corps HTTP, les surcharges async sur flux évitent de construire toute la chaîne en mémoire :

await using FileStream stream = File.OpenRead("orders.json");
List<Order> orders = await JsonSerializer.DeserializeAsync<List<Order>>(stream);

Dans ASP.NET Core et avec HttpClient, vous appelez rarement le sérialiseur vous-même : les contrôleurs lient automatiquement les corps JSON, et httpClient.GetFromJsonAsync<Order>(url) (dans System.Net.Http.Json) fait la requête et la désérialisation en un seul appel.

Erreurs

Un JSON invalide, ou une valeur qui ne peut pas être convertie vers le type de la propriété, lève JsonException. Son message indique le chemin JSON et la position, ce qui suffit en général à trouver le problème :

try
{
    JsonSerializer.Deserialize<Product>("{\"Name\": \"Lamp\", \"Price\": \"cheap\"}");
}
catch (JsonException e)
{
    Console.WriteLine(e.Message);
    // The JSON value could not be converted to System.Decimal. Path: $.Price | LineNumber: 0 | BytePositionInLine: 33.
}

Deserialize renvoie null (et non une exception) quand le texte JSON est le littéral null, donc vérifiez le résultat quand l'entrée vient de l'extérieur.

Caractères échappés dans la sortie

Par défaut, le sérialiseur échappe les caractères non ASCII et les caractères dangereux dans du HTML :

Console.WriteLine(JsonSerializer.Serialize(new { city = "São Paulo", note = "5 > 3" }));
// {"city":"S\u00E3o Paulo","note":"5 \u003E 3"}

C'est du JSON valide qui se relit comme le texte d'origine ; il est échappé pour que la sortie puisse être déposée sans risque dans une page HTML. Pour des fichiers lisibles par un humain, définissez Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (de System.Text.Encodings.Web) dans les options. Le « unsafe » ne concerne que l'insertion du résultat dans du HTML.

System.Text.Json ou Newtonsoft.Json

Newtonsoft.Json (Json.NET, la classe JsonConvert) a été la norme pendant une décennie et se trouve encore partout. Les principales différences :

System.Text.JsonNewtonsoft.Json
Disponibilitéintégré à .NET Core 3.0+ ; package NuGet pour .NET Framework 4.6.2+package NuGet pour .NET Framework et .NET
Sérialiser / désérialiserJsonSerializer.Serialize(obj) / Deserialize<T>(json)JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json)
Correspondance des nomssensible à la casse par défautinsensible à la casse
Lecture non typéeJsonDocument, JsonNodeJObject, JToken, avec requêtes JSONPath
Renommer une propriété[JsonPropertyName("x")][JsonProperty("x")]
Permissivitéstrict : ni commentaires, ni virgules finales, ni nombres entre guillemets sauf activationpermissif par défaut
Performancesplus rapide, moins d'allocations, génération de source pour le trimming et l'AOTplus lent, basé sur la réflexion

Les attributs ont des noms et des namespaces différents, donc migrer une base de code revient à un rechercher-remplacer plus des tests pour l'analyse plus stricte. Pour du code .NET nouveau, commencez par System.Text.Json.

Génération de source (.NET 6+)

JsonSerializer inspecte normalement vos types par réflexion à l'exécution. Pour les applications réduites par trimming ou compilées à l'avance (Native AOT, Blazor WebAssembly), un générateur de source écrit ce code à la compilation :

[JsonSerializable(typeof(Product))]
internal partial class AppJsonContext : JsonSerializerContext { }

string json = JsonSerializer.Serialize(lamp, AppJsonContext.Default.Product);

Erreurs courantes

  • Du JSON en camelCase vers des propriétés en PascalCase avec les options par défaut. Chaque propriété reste vide, silencieusement. Utilisez PropertyNameCaseInsensitive ou JsonSerializerDefaults.Web.
  • Des champs publics au lieu de propriétés. Ils ne sont pas sérialisés, sauf si IncludeFields est défini.
  • Des propriétés sans setter. Une propriété en lecture seule sans paramètre de constructeur correspondant n'est pas remplie à la désérialisation.
  • Un nouveau JsonSerializerOptions par appel. Réutilisez une instance statique.
  • De l'argent en virgule flottante. Un double valant 0.1 + 0.2 se sérialise en 0.30000000000000004. Utilisez decimal pour les montants.

Questions fréquentes

Comment convertir un objet en JSON en C# ?

Appelez JsonSerializer.Serialize(obj) de System.Text.Json, intégré à .NET Core 3.0 et plus sans package à installer. Il écrit chaque propriété publique : {"Name":"Desk lamp","Price":34.90}. Passez new JsonSerializerOptions { WriteIndented = true } pour une sortie lisible et PropertyNamingPolicy = JsonNamingPolicy.CamelCase pour des noms en camelCase.

Comment convertir du JSON en objet en C# ?

var product = JsonSerializer.Deserialize<Product>(json); crée un Product et remplit ses propriétés publiques modifiables à partir des noms JSON correspondants. La correspondance est sensible à la casse par défaut, donc un JSON en camelCase laisse vides des propriétés en PascalCase, sauf si vous passez PropertyNameCaseInsensitive = true ou new JsonSerializerOptions(JsonSerializerDefaults.Web). Un JSON mal formé ou une valeur du mauvais type lève JsonException.

Comment lire du JSON sans créer de classe en C# ?

Utilisez JsonDocument.Parse(json) et parcourez RootElement avec GetProperty("name"), GetString(), GetInt32() et EnumerateArray() ; c'est en lecture seule et rapide, et cela doit être libéré. Pour du JSON que vous voulez modifier, JsonNode.Parse(json) (.NET 6+) donne un arbre modifiable : node["city"], des affectations et ToJsonString().

Faut-il utiliser System.Text.Json ou Newtonsoft.Json ?

Pour du code nouveau sur .NET Core 3.0 ou plus, System.Text.Json : il est intégré, plus rapide, alloue moins, et ASP.NET Core l'utilise par défaut. Newtonsoft.Json (Json.NET) reste courant dans les projets .NET Framework (où System.Text.Json n'existe que sous forme de package NuGet), dans le code qui dépend de ses fonctionnalités supplémentaires (requêtes JSONPath, analyse très permissive, TypeNameHandling), et dans les grandes bases de code déjà construites dessus.

Pourquoi System.Text.Json échappe-t-il des caractères comme é et < ?

L'encodeur par défaut échappe les caractères non ASCII et ceux sensibles en HTML (<, >, &, ') sous la forme \uXXXX, pour que la sortie puisse être insérée sans risque dans du HTML. Cela reste du JSON valide qui se désérialise vers le même texte. Pour une sortie lisible, définissez Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping dans les options, mais seulement quand le JSON n'est pas écrit dans du HTML.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER