Menu

JSON in C#: serializzare e deserializzare con System.Text.Json

Come lavorare con JSON in C# usando System.Text.Json: JsonSerializer.Serialize e Deserialize, output camelCase e indentato, attributi come JsonPropertyName e JsonIgnore, enum come stringhe, leggere JSON senza classi con JsonDocument e JsonNode, gli errori e il confronto con Newtonsoft.Json.

JSON è il modo in cui la maggior parte dei programmi C# comunica con le API web, salva le impostazioni e scambia dati. Il .NET moderno lo legge e lo scrive con System.Text.Json, che fa parte del runtime da .NET Core 3.0 in poi: non serve nessun pacchetto NuGet. Il suo punto di ingresso principale è la classe statica JsonSerializer, che trasforma gli oggetti in stringhe JSON e viceversa.

System.Text.Json è integrato in .NET Core 3.0 e in ogni versione successiva (da .NET 5 a .NET 10). Anche i progetti su .NET Framework 4.6.2 e successivi, o su .NET Standard 2.0, possono usarlo installando il pacchetto NuGet System.Text.Json. Gli esempi di questa pagina sono mostrati come codice semplice con l'output nei commenti. Aggiungi queste direttive using per eseguirli in un progetto .NET:

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

Serializzare: da oggetto a JSON

JsonSerializer.Serialize scrive ogni proprietà pubblica di un oggetto, usando i nomi delle proprietà così come sono:

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}

Le collezioni diventano array, i dizionari con chiavi stringa diventano oggetti ({"apples":3,"pears":5}), null resta null, e DateTime diventa una stringa ISO 8601 ("2026-03-01T14:30:00"). Due comportamenti predefiniti colgono spesso di sorpresa:

  • I campi vengono saltati. Vengono serializzate solo le proprietà. Una classe con public int X; viene serializzata come {} a meno che tu non imposti IncludeFields = true nelle opzioni o trasformi il campo in una proprietà.
  • Gli enum sono numeri. OrderStatus.Shipped viene scritto come 1 (il valore sottostante dell'enum). Aggiungi JsonStringEnumConverter (vedi sotto) per scrivere "Shipped".

Deserializzare: da JSON a oggetto

JsonSerializer.Deserialize<T> crea un T e ne imposta le proprietà a partire dal 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

Le proprietà JSON senza una proprietà C# corrispondente vengono ignorate, e le proprietà C# senza un JSON corrispondente mantengono i valori predefiniti. Di default nessuna delle due cose è un errore.

La regola che fa inciampare quasi tutti: il confronto dei nomi distingue maiuscole e minuscole. La maggior parte delle API web invia camelCase, e il camelCase non corrisponde alle proprietà in 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) dà le impostazioni che usa ASP.NET Core: lettura senza distinzione tra maiuscole e minuscole, scrittura in camelCase e numeri accettati anche come stringhe tra virgolette.

La deserializzazione ha bisogno di un modo per impostare ogni valore: un setter pubblico, un accessor init o un costruttore i cui nomi di parametro corrispondono alle proprietà. Quest'ultima regola è il motivo per cui i record funzionano subito:

public record Point(int X, int Y);

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

Opzioni: camelCase e output indentato

JsonSerializerOptions controlla la denominazione, la formattazione e altro. Crea un'istanza e riusala: il serializzatore mette in cache i metadati per ogni istanza di opzioni, quindi costruire nuove opzioni a ogni chiamata è sensibilmente più lento.

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
// }

Altre opzioni utili da conoscere: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull per omettere le proprietà null, IncludeFields = true, NumberHandling per accettare numeri scritti come stringhe, e ReadCommentHandling = JsonCommentHandling.Skip più AllowTrailingCommas = true per i file di configurazione modificati a mano. .NET 8 ha aggiunto JsonNamingPolicy.SnakeCaseLower per le API che usano snake_case.

Attributi: rinominare, ignorare, enum come stringhe

Gli attributi sulla classe controllano una proprietà alla volta e prevalgono sulle opzioni:

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"}

Per scrivere ogni enum come stringa invece di marcare ogni proprietà, aggiungi il converter alle opzioni: options.Converters.Add(new JsonStringEnumConverter());.

Leggere JSON senza una classe: JsonDocument e JsonNode

Quando ti servono solo pochi valori da una risposta grande, o la sua forma cambia, fai a meno della classe. JsonDocument analizza il JSON in un albero di sola lettura di valori 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 lancia KeyNotFoundException per un nome mancante, quindi usa TryGetProperty per i campi facoltativi. JsonDocument prende in prestito memoria da un pool, ed è per questo che va rilasciato con using.

Per modificare un JSON, usa JsonNode da System.Text.Json.Nodes (.NET 6 e successivi), che fornisce un albero modificabile con indicizzatori:

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}

File e stream

Un JSON su disco è una stringa in un file, quindi i metodi per i file si combinano direttamente con il serializzatore:

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.

Per i file grandi e i corpi HTTP, gli overload asincroni con stream evitano di costruire l'intera stringa in memoria:

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

In ASP.NET Core e con HttpClient raramente chiami il serializzatore di persona: i controller collegano automaticamente i corpi JSON, e httpClient.GetFromJsonAsync<Order>(url) (in System.Net.Http.Json) esegue la richiesta e la deserializzazione in una sola chiamata.

Errori

Un JSON non valido, o un valore che non si può convertire nel tipo della proprietà, lancia JsonException. Il suo messaggio indica il percorso JSON e la posizione, e di solito basta per trovare il problema:

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 restituisce null (non un'eccezione) quando il testo JSON è il letterale null, quindi controlla il risultato quando l'input arriva dall'esterno.

Caratteri con escape nell'output

Di default, il serializzatore fa l'escape dei caratteri non ASCII e dei caratteri non sicuri dentro l'HTML:

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

È JSON valido e rileggendolo si ottiene il testo originale; l'escape serve perché l'output si possa inserire in una pagina HTML in modo sicuro. Per file leggibili da una persona, imposta Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (da System.Text.Encodings.Web) nelle opzioni. "Unsafe" si riferisce solo all'inserimento del risultato nell'HTML.

System.Text.Json e Newtonsoft.Json a confronto

Newtonsoft.Json (Json.NET, la classe JsonConvert) è stato lo standard per un decennio ed è ancora ovunque. Le differenze principali:

System.Text.JsonNewtonsoft.Json
Disponibilitàintegrato in .NET Core 3.0+; pacchetto NuGet per .NET Framework 4.6.2+pacchetto NuGet per .NET Framework e .NET
Serializzare / deserializzareJsonSerializer.Serialize(obj) / Deserialize<T>(json)JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json)
Confronto dei nomidistingue maiuscole e minuscole di defaultnon distingue maiuscole e minuscole
Lettura senza tipiJsonDocument, JsonNodeJObject, JToken, con query JSONPath
Rinominare una proprietà[JsonPropertyName("x")][JsonProperty("x")]
Tolleranzarigido: niente commenti, virgole finali o numeri tra virgolette se non abilitatipermissivo di default
Prestazionipiù veloce, meno allocazioni, generazione di sorgente per trimming e AOTpiù lento, basato sulla reflection

Gli attributi hanno nomi e namespace diversi, quindi migrare un progetto è un lavoro di cerca e sostituisci più i test per il parsing più rigido. Per il codice .NET nuovo, parti da System.Text.Json.

Generazione di sorgente (.NET 6+)

JsonSerializer normalmente ispeziona i tuoi tipi con la reflection a runtime. Per le app sottoposte a trimming o compilate in anticipo (Native AOT, Blazor WebAssembly), un generatore di sorgente scrive invece quel codice in fase di compilazione:

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

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

Errori comuni

  • JSON in camelCase su proprietà in PascalCase con le opzioni predefinite. Ogni proprietà resta vuota, in silenzio. Usa PropertyNameCaseInsensitive o JsonSerializerDefaults.Web.
  • Campi pubblici invece di proprietà. Non vengono serializzati a meno che non sia impostato IncludeFields.
  • Proprietà senza setter. Una proprietà di sola lettura senza un parametro del costruttore corrispondente non viene riempita durante la deserializzazione.
  • Un nuovo JsonSerializerOptions a ogni chiamata. Riusa un'unica istanza statica.
  • Denaro in virgola mobile. Un double con 0.1 + 0.2 viene serializzato come 0.30000000000000004. Usa decimal per gli importi.

Domande frequenti

Come converto un oggetto in JSON in C#?

Chiama JsonSerializer.Serialize(obj) da System.Text.Json, che è integrato in .NET Core 3.0 e successivi senza pacchetti da installare. Scrive ogni proprietà pubblica: {"Name":"Desk lamp","Price":34.90}. Passa new JsonSerializerOptions { WriteIndented = true } per un output leggibile e PropertyNamingPolicy = JsonNamingPolicy.CamelCase per i nomi in camelCase.

Come converto JSON in un oggetto in C#?

var product = JsonSerializer.Deserialize<Product>(json); crea un Product e ne riempie le proprietà pubbliche impostabili a partire dai nomi JSON corrispondenti. Di default il confronto dei nomi distingue maiuscole e minuscole, quindi un JSON in camelCase lascia vuote le proprietà in PascalCase a meno che tu non passi PropertyNameCaseInsensitive = true oppure new JsonSerializerOptions(JsonSerializerDefaults.Web). Un JSON malformato o un valore del tipo sbagliato lancia JsonException.

Come leggo JSON senza creare una classe in C#?

Usa JsonDocument.Parse(json) e percorri RootElement con GetProperty("name"), GetString(), GetInt32() ed EnumerateArray(); è di sola lettura e veloce, e va rilasciato. Per un JSON che vuoi modificare, JsonNode.Parse(json) (.NET 6+) ti dà un albero modificabile: node["city"], assegnazioni e ToJsonString().

Meglio System.Text.Json o Newtonsoft.Json?

Per il codice nuovo su .NET Core 3.0 o successivi, System.Text.Json: è integrato, più veloce, alloca meno memoria e ASP.NET Core lo usa di default. Newtonsoft.Json (Json.NET) è ancora comune nei progetti .NET Framework (dove System.Text.Json è disponibile solo come pacchetto NuGet), nel codice che dipende dalle sue funzionalità extra (query JSONPath, parsing molto permissivo, TypeNameHandling) e nei grandi progetti già costruiti su di esso.

Perché System.Text.Json fa l'escape di caratteri come é e <?

L'encoder predefinito fa l'escape dei caratteri non ASCII e di quelli sensibili per l'HTML (<, >, &, ') come \uXXXX, così l'output si può inserire in modo sicuro nell'HTML. Resta JSON valido e deserializzandolo si ottiene lo stesso testo. Per un output leggibile, imposta Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping nelle opzioni, ma solo quando il JSON non viene scritto dentro l'HTML.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA