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

  1. Qué es la serialización y qué problema resuelve
  2. Un vistazo al pasado: por qué JSON ganó a binario y XML
  3. System.Text.Json: serializar con JsonSerializer.Serialize
  4. Deserializar con JsonSerializer.Deserialize
  5. Personalizando el JSON: JsonSerializerOptions y [JsonPropertyName]
  6. El reto de la herencia: serializar un catálogo con Libro y Revista
  7. Persistiendo Biblioteca completa en JSON

  1. 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]

  1. 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.

  1. System.Text.Json: serializar con JsonSerializer.Serialize

El 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"}]

  1. Deserializar con JsonSerializer.Deserialize

La 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); // 2

Deserialize<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"));

  1. Personalizando el JSON: 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 como WriteIndented (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, la snake_case habitual 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#.

  1. El reto de la herencia: serializar un catálogo con Libro y Revista

Biblioteca.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 RevistaMaterialBibliotecario 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); // True

Sin [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.

  1. Persistiendo Biblioteca completa en JSON

Con 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 un List<MaterialBibliotecario> "parece" funcionar (produce JSON válido), pero la deserialización falla, porque System.Text.Json no sabe reconstruir un tipo abstract. Si el modelo tiene herencia y necesitas deserializarlo de vuelta a sus tipos concretos, estos atributos no son opcionales.
  • Confundir Serialize/Deserialize con validación de datos: la serialización no valida que los datos tengan sentido de negocio (un Disponible que 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 con set controlado, como en la lección de Encapsulamiento).
  • Olvidar que Deserialize<T> puede devolver null: comprueba siempre el resultado con is null antes de usarlo, igual que con cualquier otro tipo de referencia anulable.
  • Usar BinaryFormatter porque 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 siempre System.Text.Json (o, si el proyecto ya lo usaba antes, la alternativa de terceros Newtonsoft.Json, hoy en día también en desuso frente a la librería estándar).
  • Consejo: activa WriteIndented = true mientras 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

  1. Crea una clase Socio (la ya conocida) y serialízala a JSON con JsonSerializerOptions { WriteIndented = true }. Muestra el resultado por consola y comprueba visualmente que las claves usan los nombres de las propiedades de C#.

  2. Deserializa el JSON "[{\"Id\":1,\"Nombre\":\"Ana Martinez\"},{\"Id\":2,\"Nombre\":\"Luis Gomez\"}]" a un List<Socio> y muestra el Nombre de cada socio recuperado con un foreach.

  3. Añade [JsonPolymorphic]/[JsonDerivedType] a MaterialBibliotecario tal como se ha mostrado en esta lección. Crea un List<MaterialBibliotecario> con un Libro y una Revista, serialízalo, y deserialízalo de vuelta a un nuevo List<MaterialBibliotecario>; comprueba con is Libro/is Revista que 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#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados