Ir al contenido principal

Serialización y deserialización de objetos

La serialización es el proceso fundamental que permite convertir objetos complejos de nuestra aplicación en un formato que puede ser almacenado, transmitido o procesado por otros sistemas. Este mecanismo resulta esencial para la persistencia de datos, la comunicación entre aplicaciones y el intercambio de información a través de redes. En C#, disponemos de múltiples estrategias de serialización que van desde formatos binarios altamente eficientes hasta representaciones textuales legibles como XML y JSON.

La deserialización complementa este proceso, permitiendo reconstruir objetos a partir de sus representaciones serializadas. Esta capacidad bidireccional nos permite mantener el estado de nuestras aplicaciones, crear sistemas de cache sofisticados y establecer protocolos de comunicación robustos entre diferentes componentes de software.

En este artículo exploraremos las diferentes técnicas de serialización disponibles en .NET, desde los métodos tradicionales hasta las aproximaciones modernas, analizando sus ventajas, limitaciones y casos de uso más apropiados para cada situación.

Conceptos fundamentales de serialización

La serialización es el proceso de convertir un objeto en memoria en una secuencia de bytes o una representación textual que puede ser almacenada o transmitida. La deserialización es el proceso inverso: reconstruir el objeto original a partir de estos datos serializados.

Tipos de serialización en .NET

Tipo de serialización Formato Características Uso recomendado
Binaria Binario Compacta, rápida, específica de .NET Sistemas internos .NET
XML Texto XML Legible, estándar, multiplataforma Configuración, intercambio de datos
JSON Texto JSON Ligero, web-friendly, popular APIs REST, aplicaciones web
Protocolo personalizado Binario/Texto Optimizado para casos específicos Alta performance, protocolos propios

Requisitos para la serialización

Para que un objeto sea serializable, debe cumplir ciertos requisitos según el método de serialización elegido:

using System;

// Serialización binaria (tradicional)
[Serializable]
public class PersonaBinaria
{
    public string Nombre { get; set; }
    public int Edad { get; set; }
    
    // Campos que no se serializan
    [NonSerialized]
    private string campoTemporal = "temporal";
}

// Serialización XML
public class PersonaXml
{
    public string Nombre { get; set; }
    public int Edad { get; set; }
    // Debe tener constructor sin parámetros para XML
    public PersonaXml() { }
}

// Serialización JSON (moderna)
public class PersonaJson
{
    public string Nombre { get; set; }
    public int Edad { get; set; }
    public DateTime FechaNacimiento { get; set; }
}

Serialización XML

La serialización XML proporciona un formato estándar, legible y multiplataforma para persistir objetos. Es especialmente útil para archivos de configuración y cuando necesitamos interoperabilidad con otros sistemas.

Serialización XML básica

using System;
using System.IO;
using System.Xml.Serialization;
using System.Collections.Generic;

[Serializable]
public class Producto
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public decimal Precio { get; set; }
    public DateTime FechaCreacion { get; set; }
    public List<string> Categorias { get; set; }
    
    // Constructor requerido para XML
    public Producto()
    {
        Categorias = new List<string>();
    }
    
    public Producto(int id, string nombre, decimal precio) : this()
    {
        Id = id;
        Nombre = nombre;
        Precio = precio;
        FechaCreacion = DateTime.Now;
    }
}

class Program
{
    static void Main()
    {
        // Crear objetos de ejemplo
        var productos = new List<Producto>
        {
            new Producto(1, "Laptop Dell", 899.99m),
            new Producto(2, "Mouse Logitech", 25.50m),
            new Producto(3, "Teclado mecánico", 120.00m)
        };
        
        // Agregar categorías
        productos[0].Categorias.AddRange(new[] { "Electrónicos", "Informática" });
        productos[1].Categorias.AddRange(new[] { "Electrónicos", "Accesorios" });
        productos[2].Categorias.AddRange(new[] { "Electrónicos", "Periféricos" });

        // Serializar a XML
        SerializarAXml(productos, "productos.xml");
        
        // Deserializar desde XML
        var productosDeserializados = DeserializarDesdeXml<List<Producto>>("productos.xml");
        
        // Mostrar resultados
        MostrarProductos(productosDeserializados);
    }
    
    static void SerializarAXml<T>(T objeto, string archivo)
    {
        try
        {
            XmlSerializer serializer = new XmlSerializer(typeof(T));
            
            using (FileStream stream = new FileStream(archivo, FileMode.Create))
            {
                serializer.Serialize(stream, objeto);
            }
            
            Console.WriteLine($"Objeto serializado exitosamente en {archivo}");
            
            // Mostrar el contenido XML generado
            string contenidoXml = File.ReadAllText(archivo);
            Console.WriteLine("\nContenido XML generado:");
            Console.WriteLine(contenidoXml);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error al serializar: {ex.Message}");
        }
    }
    
    static T DeserializarDesdeXml<T>(string archivo) where T : class
    {
        try
        {
            if (!File.Exists(archivo))
            {
                Console.WriteLine($"El archivo {archivo} no existe.");
                return null;
            }
            
            XmlSerializer serializer = new XmlSerializer(typeof(T));
            
            using (FileStream stream = new FileStream(archivo, FileMode.Open))
            {
                T objeto = (T)serializer.Deserialize(stream);
                Console.WriteLine($"\nObjeto deserializado exitosamente desde {archivo}");
                return objeto;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error al deserializar: {ex.Message}");
            return null;
        }
    }
    
    static void MostrarProductos(List<Producto> productos)
    {
        if (productos == null)
        {
            Console.WriteLine("No hay productos para mostrar.");
            return;
        }
        
        Console.WriteLine($"\n=== PRODUCTOS DESERIALIZADOS ({productos.Count}) ===");
        
        foreach (var producto in productos)
        {
            Console.WriteLine($"ID: {producto.Id}");
            Console.WriteLine($"Nombre: {producto.Nombre}");
            Console.WriteLine($"Precio: {producto.Precio:C}");
            Console.WriteLine($"Fecha de creación: {producto.FechaCreacion:dd/MM/yyyy HH:mm}");
            Console.WriteLine($"Categorías: {string.Join(", ", producto.Categorias)}");
            Console.WriteLine(new string('-', 40));
        }
    }
}

Personalización de la serialización XML

using System;
using System.IO;
using System.Xml.Serialization;

[XmlRoot("Empleado")] // Personalizar el elemento raíz
public class EmpleadoXml
{
    [XmlAttribute("id")] // Serializar como atributo XML
    public int Id { get; set; }
    
    [XmlElement("NombreCompleto")] // Personalizar nombre del elemento
    public string Nombre { get; set; }
    
    [XmlElement("Puesto")]
    public string Cargo { get; set; }
    
    [XmlIgnore] // No incluir en la serialización XML
    public string ContrasenaInterna { get; set; }
    
    [XmlArray("ListaHabilidades")] // Personalizar el contenedor del array
    [XmlArrayItem("Habilidad")] // Personalizar cada elemento del array
    public string[] Habilidades { get; set; }
    
    [XmlElement("FechaContratacion")]
    public DateTime FechaIngreso { get; set; }
    
    // Constructor requerido
    public EmpleadoXml()
    {
        Habilidades = new string[0];
    }
}

class ProgramPersonalizadoXml
{
    static void Main()
    {
        var empleado = new EmpleadoXml
        {
            Id = 1001,
            Nombre = "María González Pérez",
            Cargo = "Desarrolladora Senior",
            ContrasenaInterna = "secreto123", // No aparecerá en XML
            Habilidades = new[] { "C#", ".NET Core", "Azure", "SQL Server" },
            FechaIngreso = new DateTime(2019, 6, 15)
        };

        // Serializar con personalización
        SerializarEmpleadoXml(empleado, "empleado_personalizado.xml");
        
        // Deserializar
        var empleadoDeserializado = DeserializarEmpleadoXml("empleado_personalizado.xml");
        
        if (empleadoDeserializado != null)
        {
            MostrarEmpleado(empleadoDeserializado);
        }
    }
    
    static void SerializarEmpleadoXml(EmpleadoXml empleado, string archivo)
    {
        try
        {
            XmlSerializer serializer = new XmlSerializer(typeof(EmpleadoXml));
            
            using (FileStream stream = new FileStream(archivo, FileMode.Create))
            using (StreamWriter writer = new StreamWriter(stream, System.Text.Encoding.UTF8))
            {
                serializer.Serialize(writer, empleado);
            }
            
            Console.WriteLine($"Empleado serializado en {archivo}");
            
            // Mostrar XML generado
            string xml = File.ReadAllText(archivo);
            Console.WriteLine("\nXML personalizado generado:");
            Console.WriteLine(xml);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
    
    static EmpleadoXml DeserializarEmpleadoXml(string archivo)
    {
        try
        {
            XmlSerializer serializer = new XmlSerializer(typeof(EmpleadoXml));
            
            using (FileStream stream = new FileStream(archivo, FileMode.Open))
            {
                return (EmpleadoXml)serializer.Deserialize(stream);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error al deserializar: {ex.Message}");
            return null;
        }
    }
    
    static void MostrarEmpleado(EmpleadoXml empleado)
    {
        Console.WriteLine("\n=== EMPLEADO DESERIALIZADO ===");
        Console.WriteLine($"ID: {empleado.Id}");
        Console.WriteLine($"Nombre: {empleado.Nombre}");
        Console.WriteLine($"Cargo: {empleado.Cargo}");
        Console.WriteLine($"Fecha de ingreso: {empleado.FechaIngreso:dd/MM/yyyy}");
        Console.WriteLine($"Habilidades: {string.Join(", ", empleado.Habilidades)}");
        Console.WriteLine($"Contraseña interna: '{empleado.ContrasenaInterna}' (debe estar vacía)");
    }
}

Serialización binaria moderna

Aunque la serialización binaria tradicional (BinaryFormatter) está obsoleta por razones de seguridad, podemos implementar serialización binaria personalizada o usar librerías modernas como MessagePack.

Implementación de serialización binaria personalizada

using System;
using System.IO;
using System.Text;

public class PersonaBinaria
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public DateTime FechaNacimiento { get; set; }
    public decimal Salario { get; set; }
    public bool Activo { get; set; }

    // Método para serializar a binario
    public byte[] SerializarABinario()
    {
        using (var stream = new MemoryStream())
        using (var writer = new BinaryWriter(stream, Encoding.UTF8))
        {
            // Escribir cada campo en orden específico
            writer.Write(Id);
            writer.Write(Nombre ?? string.Empty);
            writer.Write(FechaNacimiento.ToBinary());
            writer.Write(Salario);
            writer.Write(Activo);
            
            return stream.ToArray();
        }
    }

    // Método para deserializar desde binario
    public static PersonaBinaria DeserializarDesdeBinario(byte[] datos)
    {
        using (var stream = new MemoryStream(datos))
        using (var reader = new BinaryReader(stream, Encoding.UTF8))
        {
            return new PersonaBinaria
            {
                Id = reader.ReadInt32(),
                Nombre = reader.ReadString(),
                FechaNacimiento = DateTime.FromBinary(reader.ReadInt64()),
                Salario = reader.ReadDecimal(),
                Activo = reader.ReadBoolean()
            };
        }
    }
}

class ProgramBinario
{
    static void Main()
    {
        var persona = new PersonaBinaria
        {
            Id = 1,
            Nombre = "Carlos Mendoza",
            FechaNacimiento = new DateTime(1985, 8, 20),
            Salario = 45000.75m,
            Activo = true
        };

        Console.WriteLine("=== SERIALIZACIÓN BINARIA PERSONALIZADA ===");
        
        // Serializar a binario
        byte[] datosBinarios = persona.SerializarABinario();
        
        // Guardar en archivo
        File.WriteAllBytes("persona_binaria.dat", datosBinarios);
        
        Console.WriteLine($"Persona serializada a binario ({datosBinarios.Length} bytes)");
        Console.WriteLine($"Datos binarios (hex): {Convert.ToHexString(datosBinarios)}");
        
        // Deserializar desde binario
        byte[] datosLeidos = File.ReadAllBytes("persona_binaria.dat");
        PersonaBinaria personaDeserializada = PersonaBinaria.DeserializarDesdeBinario(datosLeidos);
        
        // Verificar integridad
        Console.WriteLine("\n=== VERIFICACIÓN DE INTEGRIDAD ===");
        Console.WriteLine($"ID original: {persona.Id} | Deserializado: {personaDeserializada.Id}");
        Console.WriteLine($"Nombre original: {persona.Nombre} | Deserializado: {personaDeserializada.Nombre}");
        Console.WriteLine($"Fecha original: {persona.FechaNacimiento:dd/MM/yyyy} | Deserializada: {personaDeserializada.FechaNacimiento:dd/MM/yyyy}");
        Console.WriteLine($"Salario original: {persona.Salario:C} | Deserializado: {personaDeserializada.Salario:C}");
        Console.WriteLine($"Activo original: {persona.Activo} | Deserializado: {personaDeserializada.Activo}");
        
        // Comparar tamaños
        CompararTamanos(persona);
    }
    
    static void CompararTamanos(PersonaBinaria persona)
    {
        Console.WriteLine("\n=== COMPARACIÓN DE TAMAÑOS ===");
        
        // Binario personalizado
        byte[] binario = persona.SerializarABinario();
        Console.WriteLine($"Binario personalizado: {binario.Length} bytes");
        
        // JSON (usando System.Text.Json)
        string json = System.Text.Json.JsonSerializer.Serialize(persona);
        byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
        Console.WriteLine($"JSON: {jsonBytes.Length} bytes");
        Console.WriteLine($"Contenido JSON: {json}");
        
        // XML
        var xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(PersonaBinaria));
        using (var stringWriter = new StringWriter())
        {
            xmlSerializer.Serialize(stringWriter, persona);
            string xml = stringWriter.ToString();
            byte[] xmlBytes = Encoding.UTF8.GetBytes(xml);
            Console.WriteLine($"XML: {xmlBytes.Length} bytes");
        }
    }
}

Serialización con tipos complejos y jerarquías

Cuando trabajamos con objetos que contienen otros objetos, herencia o interfaces, la serialización requiere consideraciones adicionales.

Manejo de herencia y polimorfismo

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.IO;
using System.Collections.Generic;

// Clase base abstracta
[JsonDerivedType(typeof(Empleado), typeDiscriminator: "empleado")]
[JsonDerivedType(typeof(Gerente), typeDiscriminator: "gerente")]
[JsonDerivedType(typeof(Desarrollador), typeDiscriminator: "desarrollador")]
public abstract class Persona
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public string Email { get; set; }
    
    public abstract void MostrarInformacion();
}

// Clases derivadas
public class Empleado : Persona
{
    public string Departamento { get; set; }
    public decimal SalarioBase { get; set; }
    
    public override void MostrarInformacion()
    {
        Console.WriteLine($"Empleado: {Nombre}, Depto: {Departamento}, Salario: {SalarioBase:C}");
    }
}

public class Gerente : Empleado
{
    public int NumeroEmpleadosACargo { get; set; }
    public decimal BonoGerencial { get; set; }
    
    public override void MostrarInformacion()
    {
        Console.WriteLine($"Gerente: {Nombre}, Depto: {Departamento}, Empleados a cargo: {NumeroEmpleadosACargo}, Bono: {BonoGerencial:C}");
    }
}

public class Desarrollador : Empleado
{
    public string[] LenguajesProgramacion { get; set; }
    public int AnosExperiencia { get; set; }
    
    public Desarrollador()
    {
        LenguajesProgramacion = new string[0];
    }
    
    public override void MostrarInformacion()
    {
        Console.WriteLine($"Desarrollador: {Nombre}, Lenguajes: [{string.Join(", ", LenguajesProgramacion)}], Experiencia: {AnosExperiencia} años");
    }
}

class ProgramJerarquia
{
    static void Main()
    {
        // Crear una colección polimórfica
        var personas = new List<Persona>
        {
            new Empleado
            {
                Id = 1,
                Nombre = "Ana López",
                Email = "ana.lopez@empresa.com",
                Departamento = "Recursos Humanos",
                SalarioBase = 35000
            },
            new Gerente
            {
                Id = 2,
                Nombre = "Roberto García",
                Email = "roberto.garcia@empresa.com",
                Departamento = "Tecnología",
                SalarioBase = 60000,
                NumeroEmpleadosACargo = 12,
                BonoGerencial = 15000
            },
            new Desarrollador
            {
                Id = 3,
                Nombre = "Laura Martín",
                Email = "laura.martin@empresa.com",
                Departamento = "Desarrollo",
                SalarioBase = 45000,
                LenguajesProgramacion = new[] { "C#", "JavaScript", "Python", "SQL" },
                AnosExperiencia = 5
            }
        };

        Console.WriteLine("=== SERIALIZACIÓN DE JERARQUÍA DE CLASES ===");
        
        // Serializar la colección polimórfica
        SerializarJerarquia(personas, "equipo.json");
        
        // Deserializar manteniendo los tipos
        var personasDeserializadas = DeserializarJerarquia("equipo.json");
        
        // Mostrar información utilizando polimorfismo
        MostrarEquipo(personasDeserializadas);
        
        // Analizar tipos después de la deserialización
        AnalizarTipos(personasDeserializadas);
    }
    
    static void SerializarJerarquia(List<Persona> personas, string archivo)
    {
        try
        {
            var opciones = new JsonSerializerOptions
            {
                WriteIndented = true,
                DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
            };

            string json = JsonSerializer.Serialize(personas, opciones);
            File.WriteAllText(archivo, json);
            
            Console.WriteLine($"Jerarquía serializada en {archivo}");
            Console.WriteLine("\nContenido JSON generado:");
            Console.WriteLine(json);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error al serializar: {ex.Message}");
        }
    }
    
    static List<Persona> DeserializarJerarquia(string archivo)
    {
        try
        {
            if (!File.Exists(archivo))
            {
                Console.WriteLine($"El archivo {archivo} no existe.");
                return new List<Persona>();
            }

            string json = File.ReadAllText(archivo);
            var personas = JsonSerializer.Deserialize<List<Persona>>(json);
            
            Console.WriteLine($"\n{personas?.Count ?? 0} personas deserializadas correctamente");
            return personas ?? new List<Persona>();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error al deserializar: {ex.Message}");
            return new List<Persona>();
        }
    }
    
    static void MostrarEquipo(List<Persona> personas)
    {
        Console.WriteLine("\n=== INFORMACIÓN DEL EQUIPO (POLIMORFISMO) ===");
        
        foreach (var persona in personas)
        {
            Console.WriteLine($"Tipo: {persona.GetType().Name}");
            persona.MostrarInformacion(); // Llamada polimórfica
            Console.WriteLine($"Email: {persona.Email}");
            Console.WriteLine(new string('-', 50));
        }
    }
    
    static void AnalizarTipos(List<Persona> personas)
    {
        Console.WriteLine("\n=== ANÁLISIS DE TIPOS DESPUÉS DE DESERIALIZACIÓN ===");
        
        var conteoTipos = new Dictionary<string, int>();
        decimal sueldoTotal = 0;
        int totalDesarrolladores = 0;
        
        foreach (var persona in personas)
        {
            string tipoNombre = persona.GetType().Name;
            conteoTipos[tipoNombre] = conteoTipos.GetValueOrDefault(tipoNombre, 0) + 1;
            
            // Usar pattern matching para analizar tipos específicos
            switch (persona)
            {
                case Gerente gerente:
                    sueldoTotal += gerente.SalarioBase + gerente.BonoGerencial;
                    Console.WriteLine($"Gerente {gerente.Nombre} gestiona {gerente.NumeroEmpleadosACargo} empleados");
                    break;
                    
                case Desarrollador dev:
                    sueldoTotal += dev.SalarioBase;
                    totalDesarrolladores++;
                    Console.WriteLine($"Desarrollador {dev.Nombre} conoce {dev.LenguajesProgramacion.Length} lenguajes");
                    break;
                    
                case Empleado emp:
                    sueldoTotal += emp.SalarioBase;
                    Console.WriteLine($"Empleado {emp.Nombre} en {emp.Departamento}");
                    break;
            }
        }
        
        Console.WriteLine($"\nResumen:");
        Console.WriteLine($"Total de personas: {personas.Count}");
        foreach (var kvp in conteoTipos)
        {
            Console.WriteLine($"- {kvp.Key}: {kvp.Value}");
        }
        Console.WriteLine($"Costo total en sueldos: {sueldoTotal:C}");
        Console.WriteLine($"Desarrolladores: {totalDesarrolladores}");
    }
}

Serialización personalizada avanzada

Para casos específicos donde necesitamos control total sobre el proceso de serialización, podemos implementar conversores personalizados.

Conversor personalizado para System.Text.Json

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.IO;

// Clase que necesita serialización personalizada
public class ConfiguracionCompleja
{
    public string Nombre { get; set; }
    public TimeSpan TiempoEspera { get; set; }
    public Version VersionApp { get; set; }
    public DirectoryInfo DirectorioTrabajo { get; set; }
    public Dictionary<string, object> ConfiguracionExtra { get; set; }
    
    public ConfiguracionCompleja()
    {
        ConfiguracionExtra = new Dictionary<string, object>();
    }
}

// Conversor personalizado para TimeSpan
public class TimeSpanConverter : JsonConverter<TimeSpan>
{
    public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string valor = reader.GetString();
        return TimeSpan.Parse(valor);
    }

    public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString(@"hh\:mm\:ss"));
    }
}

// Conversor personalizado para Version
public class VersionConverter : JsonConverter<Version>
{
    public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string valor = reader.GetString();
        return new Version(valor);
    }

    public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }
}

// Conversor personalizado para DirectoryInfo
public class DirectoryInfoConverter : JsonConverter<DirectoryInfo>
{
    public override DirectoryInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string ruta = reader.GetString();
        return string.IsNullOrEmpty(ruta) ? null : new DirectoryInfo(ruta);
    }

    public override void Write(Utf8JsonWriter writer, DirectoryInfo value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value?.FullName ?? "");
    }
}

class ProgramConversorPersonalizado
{
    static void Main()
    {
        var configuracion = new ConfiguracionCompleja
        {
            Nombre = "Mi Aplicación Enterprise",
            TiempoEspera = TimeSpan.FromMinutes(30),
            VersionApp = new Version(2, 1, 5, 0),
            DirectorioTrabajo = new DirectoryInfo(@"C:\MiApp\Data"),
            ConfiguracionExtra = new Dictionary<string, object>
            {
                { "maxConexiones", 100 },
                { "habilitarLog", true },
                { "nivelLog", "Info" },
                { "configuracionAvanzada", new { timeout = 5000, reintentos = 3 } }
            }
        };

        Console.WriteLine("=== SERIALIZACIÓN CON CONVERSORES PERSONALIZADOS ===");
        
        // Configurar opciones con conversores personalizados
        var opciones = new JsonSerializerOptions
        {
            WriteIndented = true,
            Converters =
            {
                new TimeSpanConverter(),
                new VersionConverter(),
                new DirectoryInfoConverter()
            }
        };

        // Serializar
        string json = JsonSerializer.Serialize(configuracion, opciones);
        Console.WriteLine("Configuración serializada:");
        Console.WriteLine(json);
        
        // Guardar en archivo
        File.WriteAllText("configuracion_compleja.json", json);
        
        // Deserializar
        string jsonLeido = File.ReadAllText("configuracion_compleja.json");
        ConfiguracionCompleja configDeserializada = JsonSerializer.Deserialize<ConfiguracionCompleja>(jsonLeido, opciones);
        
        // Verificar integridad de la deserialización
        VerificarConfiguracion(configuracion, configDeserializada);
        
        // Mostrar uso práctico
        UsarConfiguracion(configDeserializada);
    }
    
    static void VerificarConfiguracion(ConfiguracionCompleja original, ConfiguracionCompleja deserializada)
    {
        Console.WriteLine("\n=== VERIFICACIÓN DE INTEGRIDAD ===");
        
        Console.WriteLine($"Nombre: {original.Nombre} == {deserializada.Nombre} -> {original.Nombre == deserializada.Nombre}");
        Console.WriteLine($"Tiempo espera: {original.TiempoEspera} == {deserializada.TiempoEspera} -> {original.TiempoEspera == deserializada.TiempoEspera}");
        Console.WriteLine($"Versión: {original.VersionApp} == {deserializada.VersionApp} -> {original.VersionApp.ToString() == deserializada.VersionApp.ToString()}");
        Console.WriteLine($"Directorio: {original.DirectorioTrabajo?.FullName} == {deserializada.DirectorioTrabajo?.FullName} -> {original.DirectorioTrabajo?.FullName == deserializada.DirectorioTrabajo?.FullName}");
        Console.WriteLine($"Configuración extra: {original.ConfiguracionExtra.Count} elementos == {deserializada.ConfiguracionExtra.Count} elementos");
    }
    
    static void UsarConfiguracion(ConfiguracionCompleja config)
    {
        Console.WriteLine("\n=== USO PRÁCTICO DE LA CONFIGURACIÓN ===");
        
        Console.WriteLine($"Iniciando {config.Nombre} v{config.VersionApp}");
        Console.WriteLine($"Directorio de trabajo: {config.DirectorioTrabajo?.FullName}");
        Console.WriteLine($"Tiempo de espera configurado: {config.TiempoEspera}");
        
        // Usar la configuración extra
        if (config.ConfiguracionExtra.TryGetValue("maxConexiones", out object maxConn))
        {
            Console.WriteLine($"Máximo de conexiones: {maxConn}");
        }
        
        if (config.ConfiguracionExtra.TryGetValue("habilitarLog", out object logEnabled) && (bool)logEnabled)
        {
            Console.WriteLine($"Logging habilitado - Nivel: {config.ConfiguracionExtra["nivelLog"]}");
        }
        
        // Verificar si el directorio existe
        if (config.DirectorioTrabajo != null)
        {
            if (config.DirectorioTrabajo.Exists)
            {
                Console.WriteLine("✓ Directorio de trabajo existe y es accesible");
            }
            else
            {
                Console.WriteLine("⚠ Advertencia: El directorio de trabajo no existe");
            }
        }
    }
}

Técnicas de optimización y mejores prácticas

Para aplicaciones que manejan gran volumen de datos o requieren alta performance, es importante optimizar el proceso de serialización.

Optimización de memoria y performance

using System;
using System.Text.Json;
using System.IO;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Diagnostics;

public class DatosGrandes
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public double[] ValoresNumericos { get; set; }
    public DateTime FechaCreacion { get; set; }
}

class OptimizacionSerializacion
{
    static async Task Main()
    {
        Console.WriteLine("=== OPTIMIZACIÓN DE SERIALIZACIÓN ===");
        
        // Generar datos de prueba
        var datosGrandes = GenerarDatosPrueba(10000);
        Console.WriteLine($"Generados {datosGrandes.Count} registros para pruebas de performance");
        
        // Comparar diferentes métodos de serialización
        await CompararMetodosSerializacion(datosGrandes);
        
        // Demostrar serialización con streams
        await DemostrarStreaming(datosGrandes);
        
        // Optimizaciones específicas
        DemostrarOptimizaciones();
    }
    
    static List<DatosGrandes> GenerarDatosPrueba(int cantidad)
    {
        var datos = new List<DatosGrandes>();
        var random = new Random();
        
        for (int i = 0; i < cantidad; i++)
        {
            var valores = new double[50]; // Array de 50 valores por registro
            for (int j = 0; j < valores.Length; j++)
            {
                valores[j] = random.NextDouble() * 1000;
            }
            
            datos.Add(new DatosGrandes
            {
                Id = i + 1,
                Nombre = $"Registro_{i + 1:D6}",
                ValoresNumericos = valores,
                FechaCreacion = DateTime.Now.AddDays(-random.Next(365))
            });
        }
        
        return datos;
    }
    
    static async Task CompararMetodosSerializacion(List<DatosGrandes> datos)
    {
        Console.WriteLine("\n=== COMPARACIÓN DE MÉTODOS DE SERIALIZACIÓN ===");
        
        // Método 1: Serialización síncrona en memoria
        var sw = Stopwatch.StartNew();
        var opcionesCompactas = new JsonSerializerOptions { WriteIndented = false };
        string jsonEnMemoria = JsonSerializer.Serialize(datos, opcionesCompactas);
        sw.Stop();
        Console.WriteLine($"Serialización en memoria: {sw.ElapsedMilliseconds} ms, Tamaño: {jsonEnMemoria.Length / 1024} KB");
        
        // Método 2: Serialización directa a archivo
        sw.Restart();
        using (var stream = new FileStream("datos_directos.json", FileMode.Create))
        {
            await JsonSerializer.SerializeAsync(stream, datos, opcionesCompactas);
        }
        sw.Stop();
        var tamanoArchivo = new FileInfo("datos_directos.json").Length;
        Console.WriteLine($"Serialización a stream: {sw.ElapsedMilliseconds} ms, Tamaño: {tamanoArchivo / 1024} KB");
        
        // Método 3: Deserialización desde stream
        sw.Restart();
        List<DatosGrandes> datosDeserializados;
        using (var stream = new FileStream("datos_directos.json", FileMode.Open))
        {
            datosDeserializados = await JsonSerializer.DeserializeAsync<List<DatosGrandes>>(stream);
        }
        sw.Stop();
        Console.WriteLine($"Deserialización desde stream: {sw.ElapsedMilliseconds} ms, Registros: {datosDeserializados?.Count ?? 0}");
        
        // Método 4: Con opciones optimizadas
        sw.Restart();
        var opcionesOptimizadas = new JsonSerializerOptions
        {
            WriteIndented = false,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
            NumberHandling = JsonNumberHandling.AllowReadingFromString
        };
        
        using (var stream = new FileStream("datos_optimizados.json", FileMode.Create))
        {
            await JsonSerializer.SerializeAsync(stream, datos, opcionesOptimizadas);
        }
        sw.Stop();
        var tamanoOptimizado = new FileInfo("datos_optimizados.json").Length;
        Console.WriteLine($"Serialización optimizada: {sw.ElapsedMilliseconds} ms, Tamaño: {tamanoOptimizado / 1024} KB");
    }
    
    static async Task DemostrarStreaming(List<DatosGrandes> datos)
    {
        Console.WriteLine("\n=== PROCESAMIENTO CON STREAMING ===");
        
        // Simular procesamiento de streaming escribiendo un registro a la vez
        var sw = Stopwatch.StartNew();
        
        using (var stream = new FileStream("datos_streaming.json", FileMode.Create))
        using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false }))
        {
            writer.WriteStartArray();
            
            for (int i = 0; i < datos.Count; i++)
            {
                // Escribir cada objeto individualmente
                JsonSerializer.Serialize(writer, datos[i]);
                
                // Simular procesamiento adicional cada 1000 registros
                if (i > 0 && i % 1000 == 0)
                {
                    await writer.FlushAsync();
                    Console.WriteLine($"Procesados {i} registros...");
                }
            }
            
            writer.WriteEndArray();
        }
        
        sw.Stop();
        Console.WriteLine($"Streaming completado en {sw.ElapsedMilliseconds} ms");
        
        // Leer usando streaming
        await LeerConStreaming("datos_streaming.json");
    }
    
    static async Task LeerConStreaming(string archivo)
    {
        Console.WriteLine($"\nLeyendo {archivo} con streaming...");
        
        using var stream = new FileStream(archivo, FileMode.Open);
        using var document = await JsonDocument.ParseAsync(stream);
        
        var root = document.RootElement;
        if (root.ValueKind == JsonValueKind.Array)
        {
            int contador = 0;
            foreach (var elemento in root.EnumerateArray())
            {
                // Procesar cada elemento sin cargar todo en memoria
                if (elemento.TryGetProperty("Id", out var idProp) && 
                    elemento.TryGetProperty("Nombre", out var nombreProp))
                {
                    contador++;
                    
                    // Mostrar progreso cada 2500 registros
                    if (contador % 2500 == 0)
                    {
                        Console.WriteLine($"Leído registro {contador}: ID={idProp.GetInt32()}, Nombre={nombreProp.GetString()}");
                    }
                }
            }
            Console.WriteLine($"Total de registros procesados: {contador}");
        }
    }
    
    static void DemostrarOptimizaciones()
    {
        Console.WriteLine("\n=== TÉCNICAS DE OPTIMIZACIÓN ESPECÍFICAS ===");
        
        // 1. Reutilizar opciones de serialización
        var opcionesReutilizables = new JsonSerializerOptions
        {
            WriteIndented = false,
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        
        Console.WriteLine("✓ Reutilización de JsonSerializerOptions");
        
        // 2. Usar ArrayPool para reducir allocaciones
        Console.WriteLine("✓ Uso de ArrayPool recomendado para arrays grandes");
        
        // 3. Configuraciones específicas para performance
        var opcionesPerformance = new JsonSerializerOptions
        {
            DefaultBufferSize = 32 * 1024, // Buffer de 32KB para streams grandes
            MaxDepth = 32, // Limitar profundidad para evitar stack overflow
            AllowTrailingCommas = false, // Parsing más rápido
            ReadCommentHandling = JsonCommentHandling.Skip
        };
        
        Console.WriteLine("✓ Configuraciones de performance aplicadas");
        
        // 4. Demostrar medición de memoria
        MedirUsoMemoria();
    }
    
    static void MedirUsoMemoria()
    {
        Console.WriteLine("\n=== MEDICIÓN DE USO DE MEMORIA ===");
        
        // Obtener memoria inicial
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        
        long memoriaInicial = GC.GetTotalMemory(false);
        Console.WriteLine($"Memoria inicial: {memoriaInicial / 1024} KB");
        
        // Crear datos y serializar
        var datos = GenerarDatosPrueba(1000);
        string json = JsonSerializer.Serialize(datos);
        
        long memoriaConDatos = GC.GetTotalMemory(false);
        Console.WriteLine($"Memoria con datos: {memoriaConDatos / 1024} KB");
        Console.WriteLine($"Memoria adicional utilizada: {(memoriaConDatos - memoriaInicial) / 1024} KB");
        
        // Liberar referencias
        datos = null;
        json = null;
        
        // Forzar garbage collection
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        
        long memoriaFinal = GC.GetTotalMemory(false);
        Console.WriteLine($"Memoria después de GC: {memoriaFinal / 1024} KB");
        Console.WriteLine($"Memoria liberada: {(memoriaConDatos - memoriaFinal) / 1024} KB");
    }
}

Comparación de formatos de serialización

Para ayudar en la elección del formato más adecuado según las necesidades del proyecto, es útil comparar las características de cada uno.

Análisis comparativo detallado

using System;
using System.Text.Json;
using System.Xml.Serialization;
using System.IO;
using System.Diagnostics;
using System.Text;

public class ProductoComparacion
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public string Descripcion { get; set; }
    public decimal Precio { get; set; }
    public DateTime FechaCreacion { get; set; }
    public string[] Categorias { get; set; }
    public bool Disponible { get; set; }
    
    public ProductoComparacion()
    {
        Categorias = new string[0];
    }
}

class ComparacionFormatos
{
    static void Main()
    {
        var producto = new ProductoComparacion
        {
            Id = 1001,
            Nombre = "Laptop Gaming Premium",
            Descripcion = "Laptop de alto rendimiento con procesador Intel Core i9, 32GB RAM, SSD 1TB, tarjeta gráfica RTX 4080, pantalla 17.3 pulgadas 4K, teclado RGB mecánico, y sistema de refrigeración avanzado.",
            Precio = 2499.99m,
            FechaCreacion = DateTime.Now,
            Categorias = new[] { "Electrónicos", "Informática", "Gaming", "Alto Rendimiento" },
            Disponible = true
        };

        Console.WriteLine("=== COMPARACIÓN DE FORMATOS DE SERIALIZACIÓN ===");
        Console.WriteLine($"Objeto a serializar: {producto.Nombre}");
        Console.WriteLine($"Descripción: {producto.Descripcion.Substring(0, Math.Min(50, producto.Descripcion.Length))}...");
        
        // Comparar todos los formatos
        CompararJSON(producto);
        CompararXML(producto);
        CompararBinarioPersonalizado(producto);
        
        // Resumen comparativo
        MostrarResumenComparativo();
    }
    
    static void CompararJSON(ProductoComparacion producto)
    {
        Console.WriteLine("\n--- FORMATO JSON ---");
        
        var sw = Stopwatch.StartNew();
        
        // JSON compacto
        var opcionesCompactas = new JsonSerializerOptions { WriteIndented = false };
        string jsonCompacto = JsonSerializer.Serialize(producto, opcionesCompactas);
        sw.Stop();
        
        Console.WriteLine($"Tiempo de serialización: {sw.ElapsedMilliseconds} ms");
        Console.WriteLine($"Tamaño compacto: {Encoding.UTF8.GetByteCount(jsonCompacto)} bytes");
        Console.WriteLine($"JSON compacto: {jsonCompacto}");
        
        // JSON formateado
        sw.Restart();
        var opcionesFormateadas = new JsonSerializerOptions { WriteIndented = true };
        string jsonFormateado = JsonSerializer.Serialize(producto, opcionesFormateadas);
        sw.Stop();
        
        Console.WriteLine($"Tamaño formateado: {Encoding.UTF8.GetByteCount(jsonFormateado)} bytes");
        Console.WriteLine($"JSON formateado:\n{jsonFormateado}");
        
        // Deserialización
        sw.Restart();
        var productoDeserializado = JsonSerializer.Deserialize<ProductoComparacion>(jsonCompacto);
        sw.Stop();
        
        Console.WriteLine($"Tiempo de deserialización: {sw.ElapsedMilliseconds} ms");
        Console.WriteLine($"Integridad: {productoDeserializado.Nombre == producto.Nombre}");
    }
    
    static void CompararXML(ProductoComparacion producto)
    {
        Console.WriteLine("\n--- FORMATO XML ---");
        
        var serializer = new XmlSerializer(typeof(ProductoComparacion));
        var sw = Stopwatch.StartNew();
        
        // Serialización XML
        using var stringWriter = new StringWriter();
        serializer.Serialize(stringWriter, producto);
        string xml = stringWriter.ToString();
        sw.Stop();
        
        Console.WriteLine($"Tiempo de serialización: {sw.ElapsedMilliseconds} ms");
        Console.WriteLine($"Tamaño XML: {Encoding.UTF8.GetByteCount(xml)} bytes");
        Console.WriteLine($"XML generado:\n{xml}");
        
        // Deserialización XML
        sw.Restart();
        using var stringReader = new StringReader(xml);
        var productoDeserializado = (ProductoComparacion)serializer.Deserialize(stringReader);
        sw.Stop();
        
        Console.WriteLine($"Tiempo de deserialización: {sw.ElapsedMilliseconds} ms");
        Console.WriteLine($"Integridad: {productoDeserializado.Nombre == producto.Nombre}");
    }
    
    static void CompararBinarioPersonalizado(ProductoComparacion producto)
    {
        Console.WriteLine("\n--- FORMATO BINARIO PERSONALIZADO ---");
        
        var sw = Stopwatch.StartNew();
        
        // Serialización binaria personalizada
        byte[] datosBinarios;
        using (var stream = new MemoryStream())
        using (var writer = new BinaryWriter(stream, Encoding.UTF8))
        {
            writer.Write(producto.Id);
            writer.Write(producto.Nombre ?? "");
            writer.Write(producto.Descripcion ?? "");
            writer.Write(producto.Precio);
            writer.Write(producto.FechaCreacion.ToBinary());
            writer.Write(producto.Categorias.Length);
            foreach (var categoria in producto.Categorias)
            {
                writer.Write(categoria);
            }
            writer.Write(producto.Disponible);
            
            datosBinarios = stream.ToArray();
        }
        sw.Stop();
        
        Console.WriteLine($"Tiempo de serialización: {sw.ElapsedMilliseconds} ms");
        Console.WriteLine($"Tamaño binario: {datosBinarios.Length} bytes");
        Console.WriteLine($"Datos binarios (hex): {Convert.ToHexString(datosBinarios)[..Math.Min(100, Convert.ToHexString(datosBinarios).Length)]}...");
        
        // Deserialización binaria
        sw.Restart();
        ProductoComparacion productoDeserializado;
        using (var stream = new MemoryStream(datosBinarios))
        using (var reader = new BinaryReader(stream, Encoding.UTF8))
        {
            productoDeserializado = new ProductoComparacion
            {
                Id = reader.ReadInt32(),
                Nombre = reader.ReadString(),
                Descripcion = reader.ReadString(),
                Precio = reader.ReadDecimal(),
                FechaCreacion = DateTime.FromBinary(reader.ReadInt64()),
                Disponible = reader.ReadBoolean()
            };
            
            // Leer categorías
            int numCategorias = reader.ReadInt32();
            productoDeserializado.Categorias = new string[numCategorias];
            for (int i = 0; i < numCategorias; i++)
            {
                productoDeserializado.Categorias[i] = reader.ReadString();
            }
            
            // Leer disponibilidad (ya leída arriba, corregir orden)
            stream.Position -= sizeof(bool);
            var categorias = productoDeserializado.Categorias;
            productoDeserializado.Categorias = categorias;
            productoDeserializado.Disponible = reader.ReadBoolean();
        }
        sw.Stop();
        
        Console.WriteLine($"Tiempo de deserialización: {sw.ElapsedMilliseconds} ms");
        Console.WriteLine($"Integridad: {productoDeserializado.Nombre == producto.Nombre}");
    }
    
    static void MostrarResumenComparativo()
    {
        Console.WriteLine("\n=== RESUMEN COMPARATIVO ===");
        
        var comparacion = new[]
        {
            new { Formato = "JSON Compacto", Legibilidad = "Alta", Tamaño = "Medio", Velocidad = "Alta", Compatibilidad = "Excelente", UsoRecomendado = "APIs, configuración" },
            new { Formato = "JSON Formateado", Legibilidad = "Excelente", Tamaño = "Grande", Velocidad = "Alta", Compatibilidad = "Excelente", UsoRecomendado = "Desarrollo, debug" },
            new { Formato = "XML", Legibilidad = "Buena", Tamaño = "Grande", Velocidad = "Media", Compatibilidad = "Excelente", UsoRecomendado = "Estándares enterprise, SOAP" },
            new { Formato = "Binario", Legibilidad = "Nula", Tamaño = "Pequeño", Velocidad = "Excelente", Compatibilidad = "Limitada", UsoRecomendado = "Performance crítica, redes" }
        };

        Console.WriteLine($"{"Formato",-15} {"Legibilidad",-12} {"Tamaño",-8} {"Velocidad",-10} {"Compatibilidad",-13} {"Uso Recomendado",-25}");
        Console.WriteLine(new string('-', 90));
        
        foreach (var item in comparacion)
        {
            Console.WriteLine($"{item.Formato,-15} {item.Legibilidad,-12} {item.Tamaño,-8} {item.Velocidad,-10} {item.Compatibilidad,-13} {item.UsoRecomendado,-25}");
        }
        
        Console.WriteLine("\n=== RECOMENDACIONES GENERALES ===");
        Console.WriteLine("• JSON: Ideal para APIs web, configuraciones y intercambio de datos moderno");
        Console.WriteLine("• XML: Mejor para sistemas legacy y cuando se requiere validación estricta");
        Console.WriteLine("• Binario: Óptimo para alta performance y comunicación interna entre componentes .NET");
        Console.WriteLine("• Considerar el contexto: legibilidad vs performance, compatibilidad vs eficiencia");
    }
}

Resumen

La serialización y deserialización de objetos constituye un pilar fundamental en el desarrollo de aplicaciones modernas con C#. A través de este artículo hemos explorado las diferentes estrategias disponibles, desde la serialización XML tradicional hasta las técnicas binarias personalizadas y los conversores avanzados de System.Text.Json.

Cada formato de serialización tiene sus propias ventajas y limitaciones: JSON sobresale por su simplicidad y compatibilidad web, XML brinda robustez y validación estricta, mientras que los formatos binarios ofrecen máxima eficiencia en términos de velocidad y espacio. La elección del método apropiado depende de factores como los requisitos de performance, la necesidad de legibilidad, la compatibilidad con otros sistemas y las restricciones de la infraestructura.

Las técnicas de optimización que hemos cubierto, incluyendo el uso de streams para grandes volúmenes de datos, la reutilización de configuraciones y la implementación de conversores personalizados, nos permiten crear soluciones que escalen eficientemente. Con este conocimiento, estamos preparados para implementar sistemas de persistencia robustos, APIs eficientes y mecanismos de intercambio de datos que satisfagan las demandas de las aplicaciones empresariales modernas.