CodeGym /Corsi /C# SELF /Immersione profonda in Sys...

Immersione profonda in System.Text.Json

C# SELF
Livello 47 , Lezione 1
Disponibile

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
[JsonIgnore]
Esclude la proprietà dalla serializzazione/deserializzazione
[JsonPropertyName("nome")]
Usa un nome diverso nel JSON
[JsonInclude]
Include una proprietà/campo non pubblico nella serializzazione
[JsonNumberHandling]
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
WriteIndented
true — formattazione con indentazione
PropertyNameCaseInsensitive
true — ignora il case dei nomi delle proprietà in deserializzazione
PropertyNamingPolicy
JsonNamingPolicy.CamelCase
DefaultIgnoreCondition
Regole per ignorare null/valori di default
ReferenceHandler
Preserve
,
IgnoreCycles
AllowTrailingCommas
true — permette la virgola finale in un array
NumberHandling
Conversione numeri ↔ stringhe (e altro)
Converters
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.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION