1. Introduzione
In questo corso facciamo un passo da "fammi solo salvare una lista" verso una serializzazione flessibile, precisa e performante usando System.Text.Json. Nei progetti .NET moderni JSON è di fatto lo standard per lo scambio dati. Le chiamate base Serialize/Deserialize sono semplici, ma i casi reali richiedono regolazioni fini: ignorare/rinominare campi, gestire formati di date, proteggere dai cicli, lavorare con grandi volumi, converter personalizzati ecc.
Vedremo non solo i metodi di JsonSerializer, ma anche le opzioni con JsonSerializerOptions, gli attributi, il lavoro con Stream, la gestione della memoria e l'introduzione delle proprie regole di serializzazione tramite JsonConverter.
Breve storia e posizionamento di System.Text.Json
Per molto tempo in .NET ha dominato Newtonsoft.Json (Json.NET) — flessibile e maturo, ma non sempre il più veloce e leggero nelle dipendenze. Con .NET Core 3.0 è arrivato il built-in System.Text.Json: alta performance, dipendenze minime (parte della piattaforma), integrazione stretta con ASP.NET Core e sviluppo continuo con le release di .NET.
2. Classi e metodi principali
L'elemento base è la classe statica JsonSerializer, che dà due direzioni:
- Serializzazione: oggetto → stringa JSON (Serialize)
- Deserializzazione: stringa JSON → oggetto del tipo desiderato (Deserialize)
Esempio di serializzazione di un oggetto semplice
using System.Text.Json;
var person = new Person { Name = "Ivan", Age = 30 };
string jsonString = JsonSerializer.Serialize(person);
Console.WriteLine(jsonString); // {"Name":"Ivan","Age":30}
Esempio di deserializzazione
var json = "{\"Name\":\"Anna\",\"Age\":22}";
var anna = JsonSerializer.Deserialize<Person>(json);
Console.WriteLine(anna.Name); // Anna
Nota: il tipo Person è già implementato nelle lezioni precedenti — lo usiamo anche qui.
3. Controllare la serializzazione: JsonSerializerOptions
Nei progetti reali quasi sempre servono impostazioni: nomi delle proprietà in camelCase, formati di date, gestione dei cicli, valori di default ecc. Tutto questo si regola tramite JsonSerializerOptions.
Esempio di configurazione
var options = new JsonSerializerOptions
{
WriteIndented = true, // Formattare il JSON in modo leggibile (spazi e a capo)
PropertyNameCaseInsensitive = true, // Ignora il case dei nomi delle proprietà in deserializzazione
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // camelCase per le proprietà (invece di PascalCase)
};
string json = JsonSerializer.Serialize(person, options);
/*
{
"name": "Ivan",
"age": 30
}
*/
Perché è importante? La maggior parte dei framework front-end si aspetta proprio camelCase, non il PascalCase tipico di .NET.
4. Attributi: System.Text.Json.Serialization
A volte è più comodo controllare la serializzazione direttamente nel modello usando attributi. Si aggiungono a campi/proprietà per influenzare nomi, inclusione/esclusione e il trattamento dei valori.
Attributi principali
| Attributo | Cosa fa |
|---|---|
|
Esclude la proprietà dalla serializzazione/deserializzazione |
|
Usa un nome diverso nel JSON |
|
Include una proprietà/campo non pubblico nella serializzazione |
|
Controlla il trattamento dei valori numerici |
Esempio: controllo delle proprietà tramite attributi
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 = "Pëtr", Age = 45, SecretCode = 123 };
string json = JsonSerializer.Serialize(person);
// {"full_name":"Pëtr","Age":45}
Nota: SecretCode non è finito nel JSON, mentre Name è stato serializzato come "full_name".
5. Serializzazione di collezioni e oggetti annidati
Le collezioni sono semplici
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 = "Maksim", Age = 40 }
};
string jsonList = JsonSerializer.Serialize(people);
// [{"Name":"Anna","Age":20},{"Name":"Maksim","Age":40}]
Strutture annidate
public class Group
{
public string Name { get; set; }
public List<Person> Members { get; set; }
}
var group = new Group
{
Name = "Sviluppatori",
Members = new List<Person>
{
new Person { Name = "Sasha", Age = 23 },
new Person { Name = "Masha", Age = 28 }
}
};
string jsonGroup = JsonSerializer.Serialize(group, options);
/*
{
"name": "Sviluppatori",
"members": [
{ "name": "Sasha", "age": 23 },
{ "name": "Masha", "age": 28 }
]
}
*/
6. Deserializzazione: cosa è importante sapere?
var json = "[{\"Name\":\"Ivan\",\"Age\":21}]";
var list = JsonSerializer.Deserialize<List<Person>>(json);
Console.WriteLine(list[0].Name); // Ivan
Scenario comune: se nel JSON manca un campo, la proprietà corrispondente riceverà il valore di default. Campi extra nel JSON che non sono presenti nel modello vengono ignorati. Ma se i tipi non coincidono (per esempio invece di un numero arriva una stringa) — la deserializzazione lancerà un'eccezione.
7. Gestione di date, orari, formati e valori numerici
public class Meeting
{
public string Topic { get; set; }
public DateTime Time { get; set; }
}
var meeting = new Meeting { Topic = "Riunione", Time = DateTime.Now };
string json = JsonSerializer.Serialize(meeting);
// {"Topic":"Riunione","Time":"2024-06-06T20:30:00.0000000+03:00"}
Per impostazione predefinita DateTime viene serializzato in ISO 8601. Serve un altro formato (es. solo la data)? Usa una proprietà separata o un converter custom (vedi sotto).
FAQ: Per far serializzare i numeri come stringhe (es. telefoni o grandi ID), usa l'attributo [JsonNumberHandling(JsonNumberHandling.WriteAsString)].
8. Stream e lavoro con file
Puoi lavorare non solo con stringhe, ma anche con Stream — importante per grandi quantità di dati (file, rete).
Esempio di scrittura su file
using var fs = File.Create("person.json");
JsonSerializer.Serialize(fs, person);
// Non dimenticare fs.Flush() o usare using!
Esempio di lettura da file
using var fs = File.OpenRead("person.json");
var restored = JsonSerializer.Deserialize<Person>(fs);
Con gli stream sono disponibili i metodi asincroni SerializeAsync/DeserializeAsync — utili per servizi ad alto carico.
9. Converter personalizzati
Se le regole standard non bastano (formati non standard di date/numeri, valori complessi, strutture custom) — scrivi un JsonConverter.
Esempio: data solo come "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"
I converter custom sono utili per serializzare coordinate, vettori, colori, date/valute non standard, vari formati di ID ecc.
10. Dettagli utili
Gestione dei riferimenti ciclici e gerarchie profonde
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve, // Conserva tutti gli oggetti con $id/$ref
WriteIndented = true
};
Attenzione: nel JSON appariranno proprietà di supporto $id e $ref. Per lo scambio con sistemi esterni che non le capiscono, questo potrebbe non andare bene.
Differenze tra System.Text.Json e Newtonsoft.Json
System.Text.Json è ormai molto potente, ma non copre ancora tutti gli scenari di Newtonsoft.Json (costruttori privati, oggetti dinamici complessi ecc.). Per la maggior parte dei casi standard consigliamo il serializer integrato — è più veloce e senza dipendenze extra.
Lavorare interattivamente con JSON: DOM API
Quando devi "camminare" nel JSON senza avere un modello completo, usa JsonDocument e JsonElement.
using var doc = JsonDocument.Parse(jsonString);
JsonElement root = doc.RootElement;
if (root.TryGetProperty("Name", out var nameProperty))
{
Console.WriteLine(nameProperty.GetString());
}
11. Opzioni utili e loro effetti
| Proprietà | Valore/Scopo |
|---|---|
|
true — formattazione con indentazione |
|
true — ignora il case dei nomi delle proprietà in deserializzazione |
|
|
|
Regole per ignorare null/valori di default |
|
, |
|
true — permette la virgola finale in un array |
|
Conversione numeri ↔ stringhe (e altro) |
|
Elenco di converter personalizzati |
12. Errori comuni e consigli pratici
Errore №1: tipo sbagliato in deserializzazione. Se hai serializzato una lista, deserializza in una lista: List<T>, non in un singolo oggetto.
Errore №2: case dei nomi delle proprietà sbagliato. Senza configurare l'insensibilità al case le proprietà possono "non essere trovate". Usa PropertyNameCaseInsensitive o configura PropertyNamingPolicy.
Errore №3: gestione errata delle date. Il formato predefinito è ISO 8601. Serve un formato diverso — crea e applica un converter (JsonConverter<DateTime>).
Errore №4: aspettativa di serializzare campi privati/statici. Di default vengono usate le proprietà pubbliche. Per casi non standard usa gli attributi appropriati (es. [JsonInclude]).
Errore №5: non capire i valori di default. Campo mancante nel JSON → valore di default per la proprietà. Consideralo nella logica.
Errore №6: gestione sbagliata degli stream. Chiudi le risorse con using o await using per evitare leak e blocchi.
GO TO FULL VERSION