La lección anterior guardó el catálogo de BiblioTech en un fichero de texto plano con un
parseo manual, campo a campo, separado por ;. Funciona, pero es frágil: un título con un
; rompería el formato, y cada nueva estructura de datos (préstamos, socios con más
propiedades...) exigiría inventar y mantener a mano un formato de texto distinto. La
serialización resuelve esto de forma general: convierte cualquier objeto en una
representación reutilizable (hoy, casi siempre JSON) sin que el programador tenga que diseñar
ni parsear un formato propio. Esta lección presenta System.Text.Json, la librería estándar de
.NET para trabajar con JSON, y sustituye el parseo manual de la lección anterior por
serialización real de Biblioteca.Catalogo y Biblioteca.Socios.
Contenido
- Qué es la serialización y qué problema resuelve
- Un vistazo al pasado: por qué JSON ganó a binario y XML
System.Text.Json: serializar conJsonSerializer.Serialize- Deserializar con
JsonSerializer.Deserialize - Personalizando el JSON:
JsonSerializerOptionsy[JsonPropertyName] - El reto de la herencia: serializar un catálogo con
LibroyRevista - Persistiendo
Bibliotecacompleta en JSON
- Qué es la serialización y qué problema resuelve
Serializar un objeto es convertir su estado (el valor de sus propiedades) en una secuencia
de datos que se puede guardar en un fichero, enviar por red, o almacenar en una base de datos.
Deserializar es la operación inversa: reconstruir un objeto a partir de esa
representación. Es exactamente lo que la lección anterior hacía a mano con ; como separador,
solo que ahora se delega en una librería que ya resuelve, de forma genérica, casos que un
parseo manual maneja mal: texto con caracteres especiales, listas anidadas, valores ausentes...
flowchart LR
A[Objeto en memoria] -->|Serializar| B[Texto / JSON]
B -->|Deserializar| C[Objeto reconstruido]
- Un vistazo al pasado: por qué JSON ganó a binario y XML
.NET ha tenido, históricamente, varios mecanismos de serialización:
| Formato | Legible por humanos | Estado actual |
|---|---|---|
Binario (BinaryFormatter) |
No | Obsoleto y retirado por motivos de seguridad graves (permitía ejecutar código arbitrario al deserializar datos no confiables); no debe usarse en código nuevo |
XML (XmlSerializer, DataContractSerializer) |
Sí, pero verboso | Sigue existiendo, usado sobre todo en sistemas heredados o formatos que ya lo exigen (como algunos servicios SOAP) |
JSON (System.Text.Json, antes Newtonsoft.Json) |
Sí, y compacto | Estándar de facto hoy en día |
JSON se ha impuesto como formato por defecto por varias razones prácticas: es más compacto que
XML, es legible directamente por una persona (a diferencia del binario), es el formato nativo
de JavaScript y por tanto de prácticamente toda API web moderna, y System.Text.Json (incluido
en .NET desde la versión 3.0, sin necesidad de instalar ningún paquete adicional) ofrece un
rendimiento muy superior a las alternativas históricas. Por eso esta lección —y la última del
módulo, sobre APIs REST— se centran exclusivamente en JSON.
System.Text.Json: serializar con JsonSerializer.Serialize
System.Text.Json: serializar con JsonSerializer.SerializeEl punto de entrada para serializar cualquier objeto es el método estático
JsonSerializer.Serialize, del namespace System.Text.Json:
using System.Text.Json;
Socio socio = new Socio(1, "Ana Martinez");
string json = JsonSerializer.Serialize(socio);
Console.WriteLine(json);
// {"Id":1,"Nombre":"Ana Martinez"}JsonSerializer.Serialize recorre, mediante reflexión (una técnica que verás con detalle en el
Módulo 6), todas las propiedades públicas del objeto y produce un texto JSON equivalente. No
hace falta escribir ningún código de conversión manual: basta con que las propiedades sean
públicas, como ya lo son en Socio, Libro y el resto del modelo de BiblioTech.
También funciona directamente sobre colecciones completas:
List<Socio> socios = new List<Socio>
{
new Socio(1, "Ana Martinez"),
new Socio(2, "Luis Gomez")
};
string jsonSocios = JsonSerializer.Serialize(socios);
Console.WriteLine(jsonSocios);
// [{"Id":1,"Nombre":"Ana Martinez"},{"Id":2,"Nombre":"Luis Gomez"}]
- Deserializar con
JsonSerializer.Deserialize
JsonSerializer.DeserializeLa operación inversa, JsonSerializer.Deserialize<T>, reconstruye un objeto (o una colección)
a partir de un texto JSON, indicando el tipo de destino como argumento de tipo genérico
(recuerda los genéricos del Módulo 4):
string json = "{\"Id\":1,\"Nombre\":\"Ana Martinez\"}";
Socio? socio = JsonSerializer.Deserialize<Socio>(json);
Console.WriteLine(socio?.Nombre); // "Ana Martinez"
string jsonSocios = "[{\"Id\":1,\"Nombre\":\"Ana Martinez\"},{\"Id\":2,\"Nombre\":\"Luis Gomez\"}]";
List<Socio>? sociosDeserializados = JsonSerializer.Deserialize<List<Socio>>(jsonSocios);
Console.WriteLine(sociosDeserializados?.Count); // 2Deserialize<T> devuelve T? (puede devolver null si el JSON de entrada es literalmente la
cadena "null"); en la práctica, si el JSON proviene de un fichero que tu propio programa
generó, el resultado nunca será null, pero el compilador te obliga a tenerlo en cuenta,
igual que con FirstOrDefault en la lección de LINQ.
Combinado con lo aprendido en la lección anterior, un objeto se guarda y recupera de disco en solo dos líneas:
File.WriteAllText("socio.json", JsonSerializer.Serialize(socio));
Socio? socioRecuperado = JsonSerializer.Deserialize<Socio>(File.ReadAllText("socio.json"));
- Personalizando el JSON:
JsonSerializerOptions y [JsonPropertyName]
JsonSerializerOptions y [JsonPropertyName]Por defecto, JsonSerializer usa el nombre exacto de cada propiedad de C# como clave JSON
("Nombre", con mayúscula inicial). Dos mecanismos permiten personalizar esto:
JsonSerializerOptions: un objeto de configuración que se pasa como segundo argumento, con opciones comoWriteIndented(formatea el JSON con saltos de línea y sangría, mucho más legible para inspeccionar un fichero a mano).[JsonPropertyName("clave")]: un atributo (los atributos se estudian en profundidad en el Módulo 6) que se coloca sobre una propiedad para indicar qué clave JSON usar en su lugar, útil cuando el JSON debe seguir una convención distinta a PascalCase (por ejemplo, lasnake_casehabitual en muchas APIs).
using System.Text.Json;
using System.Text.Json.Serialization;
class SocioJson
{
public int Id { get; set; }
[JsonPropertyName("nombre_completo")]
public string Nombre { get; set; } = string.Empty;
}
var opciones = new JsonSerializerOptions { WriteIndented = true };
SocioJson socio = new SocioJson { Id = 1, Nombre = "Ana Martinez" };
string json = JsonSerializer.Serialize(socio, opciones);
Console.WriteLine(json);
// {
// "Id": 1,
// "nombre_completo": "Ana Martinez"
// }Para el resto de esta lección, Socio se serializa sin [JsonPropertyName] (las claves
JSON coinciden con las propiedades de C# tal cual), pero conviene conocer el atributo porque
reaparecerá en la última lección del módulo, al consumir una API REST externa cuyo JSON no
sigue, casi con toda seguridad, las convenciones de nombres de C#.
- El reto de la herencia: serializar un catálogo con
Libro y Revista
Libro y RevistaBiblioteca.Catalogo es un List<MaterialBibliotecario> que en realidad contiene una mezcla
de Libro y Revista (polimorfismo, Módulo 3). Serializar esta lista tal cual, sin más,
produce un problema: al deserializar, JsonSerializer no sabe si cada elemento del JSON
debe reconstruirse como Libro o como Revista —MaterialBibliotecario es abstract y no se
puede instanciar directamente.
Desde .NET 7, System.Text.Json resuelve esto con serialización polimórfica: se anota la
clase base con [JsonPolymorphic] y [JsonDerivedType] para cada tipo derivado, indicando un
valor de "discriminador" que se guarda junto a cada objeto y permite reconstruir el tipo
correcto al deserializar:
using System.Text.Json.Serialization;
[JsonPolymorphic(TypeDiscriminatorPropertyName = "tipo")]
[JsonDerivedType(typeof(Libro), "libro")]
[JsonDerivedType(typeof(Revista), "revista")]
abstract class MaterialBibliotecario : IPrestable, IBuscable
{
// ... Titulo, Autor, Disponible, constructor, Prestar(), Devolver(), MostrarFicha(),
// Describir(), Coincide() sin cambios ...
}Con estos atributos añadidos, serializar y deserializar List<MaterialBibliotecario> funciona
de forma transparente, incluyendo automáticamente el tipo concreto de cada elemento:
List<MaterialBibliotecario> catalogo = new List<MaterialBibliotecario>
{
new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
new Revista("National Geographic", "Varios autores", 302)
};
var opciones = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(catalogo, opciones);
Console.WriteLine(json);
// [
// { "tipo": "libro", "Isbn": "978-84-376-0495-4", "Titulo": "Rayuela", ... },
// { "tipo": "revista", "NumeroEdicion": 302, "Titulo": "National Geographic", ... }
// ]
List<MaterialBibliotecario>? catalogoRecuperado =
JsonSerializer.Deserialize<List<MaterialBibliotecario>>(json, opciones);
Console.WriteLine(catalogoRecuperado?[0] is Libro); // True
Console.WriteLine(catalogoRecuperado?[1] is Revista); // TrueSin [JsonPolymorphic]/[JsonDerivedType], JsonSerializer.Deserialize<List<MaterialBibliotecario>>
lanzaría una excepción, porque no habría forma de saber qué tipo concreto instanciar para cada
elemento del array JSON. Este mismo reto —cómo persistir un modelo con herencia— reaparecerá,
resuelto de otra forma, cuando llegues a Entity Framework más adelante en este módulo.
- Persistiendo
Biblioteca completa en JSON
Biblioteca completa en JSONCon todo lo anterior, Biblioteca reemplaza los métodos de texto plano de la lección anterior
por una versión basada en JSON, que guarda Catalogo y Socios en un único fichero mediante
una pequeña clase auxiliar que agrupa ambas colecciones:
class EstadoBiblioteca
{
public List<MaterialBibliotecario> Catalogo { get; set; } = new List<MaterialBibliotecario>();
public List<Socio> Socios { get; set; } = new List<Socio>();
}
class Biblioteca
{
// ... Catalogo, Socios, Prestamos, _sociosPorId, PrestamoRegistrado sin cambios ...
// ... AgregarMaterial, AgregarSocio, BuscarSocioPorId, RegistrarPrestamo, PrestarLibroAsync sin cambios ...
private static readonly JsonSerializerOptions OpcionesJson = new JsonSerializerOptions
{
WriteIndented = true
};
public void GuardarEstadoJson(string ruta)
{
EstadoBiblioteca estado = new EstadoBiblioteca
{
Catalogo = Catalogo,
Socios = Socios
};
string json = JsonSerializer.Serialize(estado, OpcionesJson);
File.WriteAllText(ruta, json);
}
public void CargarEstadoJson(string ruta)
{
if (!File.Exists(ruta))
{
throw new FileNotFoundException($"No se encontro el fichero de estado: {ruta}");
}
string json = File.ReadAllText(ruta);
EstadoBiblioteca? estado = JsonSerializer.Deserialize<EstadoBiblioteca>(json, OpcionesJson);
if (estado is null)
{
return;
}
Catalogo.Clear();
foreach (MaterialBibliotecario material in estado.Catalogo)
{
AgregarMaterial(material);
}
Socios.Clear();
foreach (Socio socio in estado.Socios)
{
AgregarSocio(socio);
}
}
}EstadoBiblioteca es una clase de usar y tirar, exclusivamente para este propósito: agrupa
Catalogo y Socios en un único objeto para que JsonSerializer produzca un solo fichero con
ambas colecciones, en vez de dos ficheros separados. GuardarEstadoJson y CargarEstadoJson
sustituyen directamente a GuardarCatalogoTexto/CargarCatalogoTexto de la lección anterior,
con la ventaja añadida de que ahora también persisten Socios, algo que el formato de texto
plano no contemplaba.
Errores Comunes y Consejos
- Serializar un tipo con herencia sin
[JsonPolymorphic]/[JsonDerivedType]: la serialización de unList<MaterialBibliotecario>"parece" funcionar (produce JSON válido), pero la deserialización falla, porqueSystem.Text.Jsonno sabe reconstruir un tipoabstract. Si el modelo tiene herencia y necesitas deserializarlo de vuelta a sus tipos concretos, estos atributos no son opcionales. - Confundir
Serialize/Deserializecon validación de datos: la serialización no valida que los datos tengan sentido de negocio (unDisponibleque en realidad debería ser imposible, por ejemplo); solo convierte el estado del objeto de un formato a otro. La validación sigue siendo responsabilidad de la propia clase (constructores, propiedades consetcontrolado, como en la lección de Encapsulamiento). - Olvidar que
Deserialize<T>puede devolvernull: comprueba siempre el resultado conis nullantes de usarlo, igual que con cualquier otro tipo de referencia anulable. - Usar
BinaryFormatterporque aparece en tutoriales antiguos: está obsoleto y supone un riesgo de seguridad real al deserializar datos que no controlas por completo; en código nuevo, usa siempreSystem.Text.Json(o, si el proyecto ya lo usaba antes, la alternativa de tercerosNewtonsoft.Json, hoy en día también en desuso frente a la librería estándar). - Consejo: activa
WriteIndented = truemientras desarrollas y depuras (el JSON resultante es mucho más fácil de inspeccionar a simple vista); considera desactivarlo en ficheros de producción muy grandes, donde el espacio adicional de la indentación puede importar.
Ejercicios
-
Crea una clase
Socio(la ya conocida) y serialízala a JSON conJsonSerializerOptions { WriteIndented = true }. Muestra el resultado por consola y comprueba visualmente que las claves usan los nombres de las propiedades de C#. -
Deserializa el JSON
"[{\"Id\":1,\"Nombre\":\"Ana Martinez\"},{\"Id\":2,\"Nombre\":\"Luis Gomez\"}]"a unList<Socio>y muestra elNombrede cada socio recuperado con unforeach. -
Añade
[JsonPolymorphic]/[JsonDerivedType]aMaterialBibliotecariotal como se ha mostrado en esta lección. Crea unList<MaterialBibliotecario>con unLibroy unaRevista, serialízalo, y deserialízalo de vuelta a un nuevoList<MaterialBibliotecario>; comprueba conis Libro/is Revistaque cada elemento recuperó su tipo concreto original.
Soluciones
Socio socio = new Socio(1, "Ana Martinez");
var opciones = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(socio, opciones);
Console.WriteLine(json);
// {
// "Id": 1,
// "Nombre": "Ana Martinez"
// }
string json = "[{\"Id\":1,\"Nombre\":\"Ana Martinez\"},{\"Id\":2,\"Nombre\":\"Luis Gomez\"}]";
List<Socio>? socios = JsonSerializer.Deserialize<List<Socio>>(json);
if (socios is not null)
{
foreach (Socio socio in socios)
{
Console.WriteLine(socio.Nombre);
}
}
List<MaterialBibliotecario> catalogo = new List<MaterialBibliotecario>
{
new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
new Revista("National Geographic", "Varios autores", 302)
};
var opciones = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(catalogo, opciones);
List<MaterialBibliotecario>? recuperado =
JsonSerializer.Deserialize<List<MaterialBibliotecario>>(json, opciones);
Console.WriteLine(recuperado?[0] is Libro); // True
Console.WriteLine(recuperado?[1] is Revista); // True
Conclusión
En esta lección has aprendido qué es la serialización, por qué JSON se ha convertido en el
formato estándar frente a alternativas históricas como el binario o el XML, y a usar
System.Text.Json para serializar y deserializar objetos y colecciones, incluyendo el caso más
complejo de un modelo con herencia mediante [JsonPolymorphic]/[JsonDerivedType]. Biblioteca
ya persiste Catalogo y Socios completos en un fichero JSON, con GuardarEstadoJson y
CargarEstadoJson, sustituyendo definitivamente el parseo manual de la lección anterior.
Guardar el estado entero en un fichero cada vez, sin embargo, deja de ser práctico cuando los datos crecen mucho o cuando varias partes de una aplicación necesitan leer y escribir el mismo estado de forma concurrente y consistente: ese es el terreno de las bases de datos relacionales. La siguiente lección, Conectividad con Bases de Datos, introduce ADO.NET clásico para guardar el catálogo de BiblioTech en una base de datos SQLite real, sentando las bases para Entity Framework, la lección que le sigue.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
