Serializzazione JSON con System.Text.Json in C#
Le applicazioni moderne comunicano costantemente scambiandosi dati, e il formato JSON è ormai lo standard di fatto per farlo, dalle API web ai file di configurazione. Serializzare significa trasformare un oggetto C# in una stringa JSON, mentre deserializzare è l'operazione inversa. La libreria System.Text.Json, integrata in .NET, offre un serializzatore veloce ed efficiente. In questo articolo vediamo come usarlo, come personalizzarne il comportamento e come gestire i casi più comuni.
Serializzare un oggetto
Il punto di ingresso è la classe statica JsonSerializer. Il metodo Serialize converte un oggetto nella sua rappresentazione JSON, mappando le proprietà pubbliche in campi del documento.
using System.Text.Json;
class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
}
var product = new Product { Id = 1, Name = "Cuffie", Price = 49.99m };
// Trasforma l'oggetto in una stringa JSON
string json = JsonSerializer.Serialize(product);
Console.WriteLine(json);
// {"Id":1,"Name":"Cuffie","Price":49.99}
Deserializzare
L'operazione inversa ricostruisce un oggetto a partire da una stringa JSON. Si indica il tipo di destinazione come parametro generico, e il serializzatore popola le proprietà corrispondenti. Le proprietà del JSON che non trovano corrispondenza vengono ignorate, e quelle mancanti restano al valore predefinito.
string input = "{\"Id\":2,\"Name\":\"Mouse\",\"Price\":29.90}";
// Ricostruisce l'oggetto dalla stringa JSON
Product? result = JsonSerializer.Deserialize<Product>(input);
Console.WriteLine(result?.Name); // Stampa "Mouse"
Personalizzare con le opzioni
Il comportamento del serializzatore si controlla tramite JsonSerializerOptions. Le opzioni più frequenti riguardano la formattazione leggibile, la convenzione dei nomi in camelCase, tipica del JSON, e la gestione dei valori nulli.
var options = new JsonSerializerOptions
{
// Indentazione per una lettura piu agevole
WriteIndented = true,
// Converte i nomi delle proprieta in camelCase
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
// Ignora le proprieta nulle in output
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
string json = JsonSerializer.Serialize(product, options);
// {
// "id": 1,
// "name": "Cuffie",
// "price": 49.99
// }
È importante che le stesse opzioni usate per serializzare siano coerenti in deserializzazione. Per impostazione predefinita, la corrispondenza dei nomi in deserializzazione può essere resa insensibile alle maiuscole con l'opzione PropertyNameCaseInsensitive, utile quando la sorgente usa convenzioni diverse.
Controllare le singole proprietà
Gli attributi permettono di personalizzare la mappatura proprietà per proprietà. JsonPropertyName assegna un nome specifico nel JSON, indipendente da quello della proprietà C#, mentre JsonIgnore esclude del tutto una proprietà dalla serializzazione.
using System.Text.Json.Serialization;
class User
{
[JsonPropertyName("user_id")]
public int Id { get; set; }
public string Name { get; set; } = "";
// Questa proprieta non comparira nel JSON
[JsonIgnore]
public string Password { get; set; } = "";
}
Collezioni e oggetti annidati
Il serializzatore gestisce naturalmente le strutture complesse: liste, dizionari e oggetti annidati vengono tradotti ricorsivamente nella corrispondente struttura JSON, senza configurazioni aggiuntive.
class Order
{
public int Id { get; set; }
public List<Product> Items { get; set; } = new();
public Dictionary<string, string> Metadata { get; set; } = new();
}
var order = new Order
{
Id = 100,
Items = new List<Product> { product },
Metadata = new Dictionary<string, string> { ["source"] = "web" }
};
string orderJson = JsonSerializer.Serialize(order);
I convertitori personalizzati
Quando la conversione predefinita non è adeguata, ad esempio per formattare una data in un modo specifico o gestire un tipo particolare, si può scrivere un convertitore personalizzato derivando da JsonConverter<T> e implementandone i metodi di lettura e scrittura. Il convertitore si registra poi tra le opzioni.
class DateOnlyConverter : JsonConverter<DateTime>
{
private const string Format = "yyyy-MM-dd";
public override DateTime Read(ref Utf8JsonReader reader,
Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.ParseExact(reader.GetString()!, Format, null);
}
public override void Write(Utf8JsonWriter writer,
DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(Format));
}
}
Serializzazione asincrona su stream
Per volumi di dati significativi, serializzare direttamente su uno stream, come vedremmo scrivendo su un file o su una risposta HTTP, evita di costruire l'intera stringa in memoria. I metodi asincroni SerializeAsync e DeserializeAsync integrano questa operazione con il modello asincrono studiato in precedenza.
using var stream = File.Create("order.json");
// Serializza direttamente sullo stream, senza stringa intermedia
await JsonSerializer.SerializeAsync(stream, order);
Un cenno alle alternative
Prima dell'arrivo di System.Text.Json, la libreria di riferimento era Newtonsoft.Json, tuttora diffusa e ricca di funzionalità. La libreria integrata è generalmente più veloce e con un minor consumo di memoria, ed è la scelta predefinita nei nuovi progetti. Newtonsoft resta valida per scenari che richiedono le sue funzionalità più avanzate o per compatibilità con codice esistente.
Conclusione
Abbiamo imparato a serializzare e deserializzare oggetti con System.Text.Json, a personalizzarne il comportamento tramite le opzioni e gli attributi JsonPropertyName e JsonIgnore, a gestire strutture complesse e a scrivere convertitori personalizzati per casi particolari. Abbiamo visto la serializzazione asincrona su stream e collocato la libreria rispetto all'alternativa Newtonsoft. Nel prossimo articolo ci occuperemo di configurazione e logging, elementi infrastrutturali indispensabili in ogni applicazione di produzione.