La lliçó anterior va desar el catàleg de BiblioTech en un fitxer de text pla amb un parseig
manual, camp a camp, separat per ;. Funciona, però és fràgil: un títol amb un ; trencaria
el format, i cada nova estructura de dades (préstecs, socis amb més propietats...) exigiria
inventar i mantenir a mà un format de text diferent. La serialització resol això de forma
general: converteix qualsevol objecte en una representació reutilitzable (avui dia, gairebé
sempre JSON) sense que el programador hagi de dissenyar ni parsejar cap format propi. Aquesta
lliçó presenta System.Text.Json, la biblioteca estàndard de .NET per treballar amb JSON, i
substitueix el parseig manual de la lliçó anterior per serialització real de
Biblioteca.Cataleg i Biblioteca.Socis.
Contingut
- Què és la serialització i quin problema resol
- Una ullada al passat: per què el JSON va guanyar al binari i a l'XML
System.Text.Json: serialitzar ambJsonSerializer.Serialize- Deserialitzar amb
JsonSerializer.Deserialize - Personalitzant el JSON:
JsonSerializerOptionsi[JsonPropertyName] - El repte de l'herència: serialitzar un catàleg amb
LlibreiRevista - Persistint
Bibliotecacompleta en JSON
- Què és la serialització i quin problema resol
Serialitzar un objecte és convertir el seu estat (el valor de les seves propietats) en una
seqüència de dades que es pot desar en un fitxer, enviar per xarxa, o emmagatzemar en una base
de dades. Deserialitzar és l'operació inversa: reconstruir un objecte a partir d'aquesta
representació. És exactament el que la lliçó anterior feia a mà amb ; com a separador,
només que ara es delega en una biblioteca que ja resol, de forma genèrica, casos que un
parseig manual gestiona malament: text amb caràcters especials, llistes imbricades, valors
absents...
flowchart LR
A[Objecte en memoria] -->|Serialitzar| B[Text / JSON]
B -->|Deserialitzar| C[Objecte reconstruit]
- Una ullada al passat: per què el JSON va guanyar al binari i a l'XML
.NET ha tingut, històricament, diversos mecanismes de serialització:
| Format | Llegible per humans | Estat actual |
|---|---|---|
Binari (BinaryFormatter) |
No | Obsolet i retirat per motius de seguretat greus (permetia executar codi arbitrari en deserialitzar dades no confiables); no s'ha d'usar en codi nou |
XML (XmlSerializer, DataContractSerializer) |
Sí, però verbós | Continua existint, usat sobretot en sistemes heretats o formats que ja l'exigeixen (com alguns serveis SOAP) |
JSON (System.Text.Json, abans Newtonsoft.Json) |
Sí, i compacte | Estàndard de facto avui en dia |
El JSON s'ha imposat com a format per defecte per diverses raons pràctiques: és més compacte
que l'XML, és llegible directament per una persona (a diferència del binari), és el format
natiu de JavaScript i per tant de pràcticament tota API web moderna, i System.Text.Json
(inclòs a .NET des de la versió 3.0, sense necessitat d'instal·lar cap paquet addicional)
ofereix un rendiment molt superior a les alternatives històriques. Per això aquesta lliçó —i
l'última del mòdul, sobre APIs REST— se centren exclusivament en JSON.
System.Text.Json: serialitzar amb JsonSerializer.Serialize
System.Text.Json: serialitzar amb JsonSerializer.SerializeEl punt d'entrada per serialitzar qualsevol objecte és el mètode estàtic
JsonSerializer.Serialize, de l'espai de noms System.Text.Json:
using System.Text.Json;
Soci soci = new Soci(1, "Anna Martinez");
string json = JsonSerializer.Serialize(soci);
Console.WriteLine(json);
// {"Id":1,"Nom":"Anna Martinez"}JsonSerializer.Serialize recorre, mitjançant reflexió (una tècnica que veuràs en detall al
Mòdul 6), totes les propietats públiques de l'objecte i produeix un text JSON equivalent. No
cal escriure cap codi de conversió manual: n'hi ha prou que les propietats siguin públiques,
com ja ho són a Soci, Llibre i la resta del model de BiblioTech.
També funciona directament sobre col·leccions completes:
List<Soci> socis = new List<Soci>
{
new Soci(1, "Anna Martinez"),
new Soci(2, "Lluis Gomez")
};
string jsonSocis = JsonSerializer.Serialize(socis);
Console.WriteLine(jsonSocis);
// [{"Id":1,"Nom":"Anna Martinez"},{"Id":2,"Nom":"Lluis Gomez"}]
- Deserialitzar amb
JsonSerializer.Deserialize
JsonSerializer.DeserializeL'operació inversa, JsonSerializer.Deserialize<T>, reconstrueix un objecte (o una col·lecció)
a partir d'un text JSON, indicant el tipus de destinació com a argument de tipus genèric
(recorda els genèrics del Mòdul 4):
string json = "{\"Id\":1,\"Nom\":\"Anna Martinez\"}";
Soci? soci = JsonSerializer.Deserialize<Soci>(json);
Console.WriteLine(soci?.Nom); // "Anna Martinez"
string jsonSocis = "[{\"Id\":1,\"Nom\":\"Anna Martinez\"},{\"Id\":2,\"Nom\":\"Lluis Gomez\"}]";
List<Soci>? socisDeserialitzats = JsonSerializer.Deserialize<List<Soci>>(jsonSocis);
Console.WriteLine(socisDeserialitzats?.Count); // 2Deserialize<T> retorna T? (pot retornar null si el JSON d'entrada és literalment la
cadena "null"); a la pràctica, si el JSON prové d'un fitxer que el teu propi programa va
generar, el resultat mai serà null, però el compilador t'obliga a tenir-ho en compte, igual
que amb FirstOrDefault a la lliçó de LINQ.
Combinat amb el que has après a la lliçó anterior, un objecte es desa i es recupera del disc en només dues línies:
File.WriteAllText("soci.json", JsonSerializer.Serialize(soci));
Soci? sociRecuperat = JsonSerializer.Deserialize<Soci>(File.ReadAllText("soci.json"));
- Personalitzant el JSON:
JsonSerializerOptions i [JsonPropertyName]
JsonSerializerOptions i [JsonPropertyName]Per defecte, JsonSerializer fa servir el nom exacte de cada propietat de C# com a clau JSON
("Nom", amb majúscula inicial). Dos mecanismes permeten personalitzar això:
JsonSerializerOptions: un objecte de configuració que es passa com a segon argument, amb opcions comWriteIndented(formata el JSON amb salts de línia i sagnat, molt més llegible per inspeccionar un fitxer a mà).[JsonPropertyName("clau")]: un atribut (els atributs s'estudien en profunditat al Mòdul 6) que es col·loca sobre una propietat per indicar quina clau JSON fer servir en el seu lloc, útil quan el JSON ha de seguir una convenció diferent de PascalCase (per exemple, elsnake_casehabitual en moltes APIs).
using System.Text.Json;
using System.Text.Json.Serialization;
class SociJson
{
public int Id { get; set; }
[JsonPropertyName("nom_complet")]
public string Nom { get; set; } = string.Empty;
}
var opcions = new JsonSerializerOptions { WriteIndented = true };
SociJson soci = new SociJson { Id = 1, Nom = "Anna Martinez" };
string json = JsonSerializer.Serialize(soci, opcions);
Console.WriteLine(json);
// {
// "Id": 1,
// "nom_complet": "Anna Martinez"
// }Per a la resta d'aquesta lliçó, Soci es serialitza sense [JsonPropertyName] (les claus
JSON coincideixen amb les propietats de C# tal qual), però convé conèixer l'atribut perquè
reapareixerà a l'última lliçó del mòdul, en consumir una API REST externa el JSON de la qual
no segueix, gairebé amb tota seguretat, les convencions de noms de C#.
- El repte de l'herència: serialitzar un catàleg amb
Llibre i Revista
Llibre i RevistaBiblioteca.Cataleg és un List<MaterialBibliotecari> que en realitat conté una barreja de
Llibre i Revista (polimorfisme, Mòdul 3). Serialitzar aquesta llista tal qual, sense res
més, produeix un problema: en deserialitzar, JsonSerializer no sap si cada element del
JSON s'ha de reconstruir com a Llibre o com a Revista —MaterialBibliotecari és
abstract i no es pot instanciar directament.
Des de .NET 7, System.Text.Json resol això amb serialització polimòrfica: s'anota la
classe base amb [JsonPolymorphic] i [JsonDerivedType] per a cada tipus derivat, indicant
un valor de "discriminador" que es desa junt amb cada objecte i permet reconstruir el tipus
correcte en deserialitzar:
using System.Text.Json.Serialization;
[JsonPolymorphic(TypeDiscriminatorPropertyName = "tipus")]
[JsonDerivedType(typeof(Llibre), "llibre")]
[JsonDerivedType(typeof(Revista), "revista")]
abstract class MaterialBibliotecari : IPrestable, ICercable
{
// ... Titol, Autor, Disponible, constructor, Prestar(), Retornar(), MostrarFitxa(),
// Descriure(), Coincideix() sense canvis ...
}Amb aquests atributs afegits, serialitzar i deserialitzar List<MaterialBibliotecari>
funciona de forma transparent, incloent automàticament el tipus concret de cada element:
List<MaterialBibliotecari> cataleg = new List<MaterialBibliotecari>
{
new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
new Revista("National Geographic", "Varios autores", 302)
};
var opcions = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(cataleg, opcions);
Console.WriteLine(json);
// [
// { "tipus": "llibre", "Isbn": "978-84-376-0495-4", "Titol": "Rayuela", ... },
// { "tipus": "revista", "NumeroEdicio": 302, "Titol": "National Geographic", ... }
// ]
List<MaterialBibliotecari>? catalegRecuperat =
JsonSerializer.Deserialize<List<MaterialBibliotecari>>(json, opcions);
Console.WriteLine(catalegRecuperat?[0] is Llibre); // True
Console.WriteLine(catalegRecuperat?[1] is Revista); // TrueSense [JsonPolymorphic]/[JsonDerivedType],
JsonSerializer.Deserialize<List<MaterialBibliotecari>> llançaria una excepció, perquè no hi
hauria manera de saber quin tipus concret instanciar per a cada element de l'array JSON.
Aquest mateix repte —com persistir un model amb herència— reapareixerà, resolt d'una altra
manera, quan arribis a Entity Framework més endavant en aquest mòdul.
- Persistint
Biblioteca completa en JSON
Biblioteca completa en JSONAmb tot l'anterior, Biblioteca reemplaça els mètodes de text pla de la lliçó anterior per
una versió basada en JSON, que desa Cataleg i Socis en un únic fitxer mitjançant una
petita classe auxiliar que agrupa totes dues col·leccions:
class EstatBiblioteca
{
public List<MaterialBibliotecari> Cataleg { get; set; } = new List<MaterialBibliotecari>();
public List<Soci> Socis { get; set; } = new List<Soci>();
}
class Biblioteca
{
// ... Cataleg, Socis, Prestecs, _socisPerId, PrestecRegistrat sense canvis ...
// ... AfegirMaterial, AfegirSoci, CercarSociPerId, RegistrarPrestec, PrestarLlibreAsync sense canvis ...
private static readonly JsonSerializerOptions OpcionsJson = new JsonSerializerOptions
{
WriteIndented = true
};
public void DesarEstatJson(string ruta)
{
EstatBiblioteca estat = new EstatBiblioteca
{
Cataleg = Cataleg,
Socis = Socis
};
string json = JsonSerializer.Serialize(estat, OpcionsJson);
File.WriteAllText(ruta, json);
}
public void CarregarEstatJson(string ruta)
{
if (!File.Exists(ruta))
{
throw new FileNotFoundException($"No s'ha trobat el fitxer d'estat: {ruta}");
}
string json = File.ReadAllText(ruta);
EstatBiblioteca? estat = JsonSerializer.Deserialize<EstatBiblioteca>(json, OpcionsJson);
if (estat is null)
{
return;
}
Cataleg.Clear();
foreach (MaterialBibliotecari material in estat.Cataleg)
{
AfegirMaterial(material);
}
Socis.Clear();
foreach (Soci soci in estat.Socis)
{
AfegirSoci(soci);
}
}
}EstatBiblioteca és una classe d'usar i llençar, exclusivament per a aquest propòsit: agrupa
Cataleg i Socis en un únic objecte perquè JsonSerializer produeixi un sol fitxer amb
totes dues col·leccions, en lloc de dos fitxers separats. DesarEstatJson i
CarregarEstatJson substitueixen directament DesarCatalegText/CarregarCatalegText de la
lliçó anterior, amb l'avantatge afegit que ara també persisteixen Socis, cosa que el format
de text pla no contemplava.
Errors Comuns i Consells
- Serialitzar un tipus amb herència sense
[JsonPolymorphic]/[JsonDerivedType]: la serialització d'unList<MaterialBibliotecari>"sembla" funcionar (produeix JSON vàlid), però la deserialització falla, perquèSystem.Text.Jsonno sap reconstruir un tipusabstract. Si el model té herència i necessites deserialitzar-lo de tornada als seus tipus concrets, aquests atributs no són opcionals. - Confondre
Serialize/Deserializeamb validació de dades: la serialització no valida que les dades tinguin sentit de negoci (unDisponibleque en realitat hauria de ser impossible, per exemple); només converteix l'estat de l'objecte d'un format a un altre. La validació continua sent responsabilitat de la mateixa classe (constructors, propietats ambsetcontrolat, com a la lliçó d'Encapsulació). - Oblidar que
Deserialize<T>pot retornarnull: comprova sempre el resultat ambis nullabans d'utilitzar-lo, igual que amb qualsevol altre tipus de referència anul·lable. - Fer servir
BinaryFormatterperquè apareix en tutorials antics: està obsolet i suposa un risc de seguretat real en deserialitzar dades que no controles del tot; en codi nou, fes servir sempreSystem.Text.Json(o, si el projecte ja el feia servir abans, l'alternativa de tercersNewtonsoft.Json, avui dia també en desús davant la biblioteca estàndard). - Consell: activa
WriteIndented = truementre desenvolupes i depures (el JSON resultant és molt més fàcil d'inspeccionar a simple vista); considera desactivar-ho en fitxers de producció molt grans, on l'espai addicional del sagnat pot importar.
Exercicis
-
Crea una classe
Soci(la ja coneguda) i serialitza-la a JSON ambJsonSerializerOptions { WriteIndented = true }. Mostra el resultat per consola i comprova visualment que les claus fan servir els noms de les propietats de C#. -
Deserialitza el JSON
"[{\"Id\":1,\"Nom\":\"Anna Martinez\"},{\"Id\":2,\"Nom\":\"Lluis Gomez\"}]"a unList<Soci>i mostra elNomde cada soci recuperat amb unforeach. -
Afegeix
[JsonPolymorphic]/[JsonDerivedType]aMaterialBibliotecarital com s'ha mostrat en aquesta lliçó. Crea unList<MaterialBibliotecari>amb unLlibrei unaRevista, serialitza'l, i deserialitza'l de tornada a un nouList<MaterialBibliotecari>; comprova ambis Llibre/is Revistaque cada element va recuperar el seu tipus concret original.
Solucions
Soci soci = new Soci(1, "Anna Martinez");
var opcions = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(soci, opcions);
Console.WriteLine(json);
// {
// "Id": 1,
// "Nom": "Anna Martinez"
// }
string json = "[{\"Id\":1,\"Nom\":\"Anna Martinez\"},{\"Id\":2,\"Nom\":\"Lluis Gomez\"}]";
List<Soci>? socis = JsonSerializer.Deserialize<List<Soci>>(json);
if (socis is not null)
{
foreach (Soci soci in socis)
{
Console.WriteLine(soci.Nom);
}
}
List<MaterialBibliotecari> cataleg = new List<MaterialBibliotecari>
{
new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
new Revista("National Geographic", "Varios autores", 302)
};
var opcions = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(cataleg, opcions);
List<MaterialBibliotecari>? recuperat =
JsonSerializer.Deserialize<List<MaterialBibliotecari>>(json, opcions);
Console.WriteLine(recuperat?[0] is Llibre); // True
Console.WriteLine(recuperat?[1] is Revista); // True
Conclusió
En aquesta lliçó has après què és la serialització, per què el JSON s'ha convertit en el
format estàndard davant alternatives històriques com el binari o l'XML, i a fer servir
System.Text.Json per serialitzar i deserialitzar objectes i col·leccions, incloent el cas més
complex d'un model amb herència mitjançant [JsonPolymorphic]/[JsonDerivedType].
Biblioteca ja persisteix Cataleg i Socis complets en un fitxer JSON, amb
DesarEstatJson i CarregarEstatJson, substituint definitivament el parseig manual de la
lliçó anterior.
Desar l'estat sencer en un fitxer cada vegada, però, deixa de ser pràctic quan les dades creixen molt o quan diverses parts d'una aplicació necessiten llegir i escriure el mateix estat de forma concurrent i consistent: aquest és el terreny de les bases de dades relacionals. La lliçó següent, Connectivitat amb Bases de Dades, introdueix l'ADO.NET clàssic per desar el catàleg de BiblioTech en una base de dades SQLite real, posant els fonaments per a Entity Framework, la lliçó que la segueix.
Curs de Programació en C#
Mòdul 1: Introducció al C#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
