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 impostiIncludeFields = truenelle opzioni o trasformi il campo in una proprietà. - Gli enum sono numeri.
OrderStatus.Shippedviene scritto come1(il valore sottostante dell'enum). AggiungiJsonStringEnumConverter(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.Json | Newtonsoft.Json | |
|---|---|---|
| Disponibilità | integrato in .NET Core 3.0+; pacchetto NuGet per .NET Framework 4.6.2+ | pacchetto NuGet per .NET Framework e .NET |
| Serializzare / deserializzare | JsonSerializer.Serialize(obj) / Deserialize<T>(json) | JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json) |
| Confronto dei nomi | distingue maiuscole e minuscole di default | non distingue maiuscole e minuscole |
| Lettura senza tipi | JsonDocument, JsonNode | JObject, JToken, con query JSONPath |
| Rinominare una proprietà | [JsonPropertyName("x")] | [JsonProperty("x")] |
| Tolleranza | rigido: niente commenti, virgole finali o numeri tra virgolette se non abilitati | permissivo di default |
| Prestazioni | più veloce, meno allocazioni, generazione di sorgente per trimming e AOT | più 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
PropertyNameCaseInsensitiveoJsonSerializerDefaults.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
JsonSerializerOptionsa ogni chiamata. Riusa un'unica istanza statica. - Denaro in virgola mobile. Un
doublecon0.1 + 0.2viene serializzato come0.30000000000000004. Usadecimalper 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.