CodeGym /Cursos /C# SELF /Serialización de objetos anidados y jerárquicos

Serialización de objetos anidados y jerárquicos

C# SELF
Nivel 46 , Lección 2
Disponible

1. Introducción

Imagina una aplicación para una librería: los objetos no son solo libros, sino también autores con biografías, editoriales que publican libros, empleados, secciones... Si la serialización solo soportara objetos "planos", nuestra app se quedaría en el nivel de una libreta de direcciones. En proyectos reales los datos suelen ser multinivel y anidados. Por eso saber serializar y deserializar estructuras jerárquicas es una habilidad que distingue a un desarrollador promedio de un verdadero maestro de la serialización en .NET.

Veremos cómo los serializadores modernos de C# (usando System.Text.Json como ejemplo) permiten guardar no solo árboles de objetos, sino selvas enteras. Y también cómo diseñar clases para serializar correctamente, para no tener que luego extraer datos manualmente a pedazos.

2. Modelado de estructuras jerárquicas

Ampliemos nuestro modelo. En ejemplos anteriores teníamos objetos Book, Author y una clase Library que guarda la lista de libros. Ahora añadimos otro nivel: cada autor tendrá una lista de libros que escribió (sí, sí, se duplica información — más adelante discutiremos a qué puede llevar esto), y la biblioteca tendrá además una lista de empleados Employee.

Diagrama de clases

Así más o menos quedará nuestra estructura de objetos:

classDiagram
    class Library {
        List~Book~ Books
        List~Employee~ Employees
        string Name
    }
    class Book {
        string Title
        Author Author
        int Year
    }
    class Author {
        string Name
        int BirthYear
        List~Book~ Books
    }
    class Employee {
        string Name
        string Position
    }

    Library "1" -- "many" Book
    Library "1" -- "many" Employee
    Book "1" -- "1" Author
    Author "1" -- "many" Book

Este enfoque nos permite implementar un sistema de biblioteca completo. Sí, existe el riesgo de referencias "encadenadas" (por ejemplo, el autor tiene libros y cada libro tiene autor: ¡serialización infinita!). Hablaremos de eso más adelante.

3. Ejemplo de implementación de clases

Primero describimos las clases C# para nuestro modelo. De paso añadimos comentarios:

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

public class Library
{
    public string Name { get; set; }
    public List<Book> Books { get; set; } = new();
    public List<Employee> Employees { get; set; } = new();
}

public class Book
{
    public string Title { get; set; }
    public int Year { get; set; }

    public Author Author { get; set; }
}

public class Author
{
    public string Name { get; set; }
    public int BirthYear { get; set; }
    
    // IMPORTANTE! Este campo es potencialmente "peligroso": puede crear una referencia cíclica.
    // Pero para la demostración lo incluimos.
    public List<Book> Books { get; set; } = new();
}

public class Employee
{
    public string Name { get; set; }
    public string Position { get; set; }
}

4. Montando la estructura: creamos la biblioteca

Ahora creamos una biblioteca con libros, autores y empleados para probar la serialización.

// Creamos autores
var author1 = new Author { Name = "Vilyam Golding", BirthYear = 1911 };
var author2 = new Author { Name = "Dzhon Updayk", BirthYear = 1932 };
var author3 = new Author { Name = "Dzhyerom Selingdjer", BirthYear = 1919 };

// Creamos libros
var book1 = new Book { Title = "Povelitely mukh", Year = 1954, Author = author1 };
var book2 = new Book { Title = "Kentavr", Year = 1963, Author = author2 };
var book3 = new Book { Title = "Nad propastyu vo rzhi", Year = 1951, Author = author3 };

// Añadimos libros a los autores
author1.Books.Add(book1);
author2.Books.Add(book2);
author3.Books.Add(book3);

// Creamos empleados
var emp1 = new Employee { Name = "Ivan Ivanov", Position = "Bibliotekar'" };
var emp2 = new Employee { Name = "Sergey Sergeev", Position = "Direktor" };

// Montamos la biblioteca
var library = new Library
{
    Name = "Gorodskaya biblioteka",
    Books = new List<Book> { book1, book2, book3 },
    Employees = new List<Employee> { emp1, emp2 }
};

Claro, en código real automatizarías la creación de objetos y sus relaciones para no mantener manualmente cada libro con su autor. Pero para el ejemplo esto basta.

5. Serialización a JSON

Usamos el enfoque clásico con System.Text.Json:

using System.Text.Json;

// Serializamos la biblioteca a JSON
var options = new JsonSerializerOptions
{
    WriteIndented = true, // Formato "bonito" con indentación
    ReferenceHandler = ReferenceHandler.IgnoreCycles // ¡Evitamos ciclos!
};

string json = JsonSerializer.Serialize(library, options);

// Mostramos el resultado
Console.WriteLine(json);

Detalle interesante: Si no usas la opción especial ReferenceHandler.IgnoreCycles, la serialización se puede ciclar — porque el autor tiene una lista de libros y cada libro tiene autor, el serializador podría caminar indefinidamente (y lanzar una excepción). La opción IgnoreCycles resuelve el problema: si durante el recorrido el serializador ve que un objeto ya se serializó más arriba en el árbol, escribe simplemente null en lugar de serializarlo de nuevo.

¿Cómo quedará el JSON?

{
  "Name": "Gorodskaya biblioteka",
  "Books": [
    {
      "Title": "Kentavr",
      "Year": 1963,
      "Author": {
        "Name": "Dzhon Updayk",
        "BirthYear": 1932,
        "Books": [
          {
            "Title": "Kentavr",
            "Year": 1963,
            "Author": null
          },
          {
            "Title": "Istvikskiye ved'my",
            "Year": 1984,
            "Author": null
          }
        ]
      }
    },
    {
      "Title": "Istvikskiye ved'my",
      "Year": 1984,
      "Author": {
        "Name": "Dzhon Updayk",
        "BirthYear": 1932,
        "Books": [
          {
            "Title": "Kentavr",
            "Year": 1963,
            "Author": null
          },
          {
            "Title": "Istvikskiye ved'my",
            "Year": 1984,
            "Author": null
          }
        ]
      }
    }
  ],
  "Employees": [
    {
      "Name": "Ivan Ivanov",
      "Position": "Bibliotekar'"
    },
    {
      "Name": "Sergey Sergeev",
      "Position": "Direktor"
    }
  ]
}

Fíjate: dentro del array Books, en los libros listados dentro del autor ya no hay info del autor — en su lugar Author: null. Esa es la forma de romper el ciclo durante la serialización.

6. Deserialización de vuelta a objetos

Ahora deserializamos los datos de vuelta:

// Restauramos el objeto desde JSON
var libraryCopy = JsonSerializer.Deserialize<Library>(json, options);

Console.WriteLine(libraryCopy.Name); // "Gorodskaya biblioteka"
Console.WriteLine($"Knig: {libraryCopy.Books.Count}");
Console.WriteLine($"Sotrudnikov: {libraryCopy.Employees.Count}");

¡Pero ojo! Restaurar referencias cíclicas no funciona al 100%: en los libros anidados dentro de la lista del autor la propiedad Author quedará en null, porque el serializador cortó la cadena para prevenir profundidad infinita.

Punto importante: Serializar relaciones mutuas complejas (por ejemplo, padre — hijos — padre) con el serializador estándar siempre requiere un compromiso: o se pierden algunas relaciones, o hay que reconstruirlas manualmente tras la deserialización.

7. Referencias cíclicas (anidamiento vs ciclos)

Si tu estructura contiene casos en que un objeto se referencia a sí mismo a través de una cadena de otros objetos (referencia cíclica), los serializadores estándar como System.Text.Json y Newtonsoft.Json, especialmente en modo fuertemente tipado, reaccionan de forma distinta. Antes de la opción ReferenceHandler.IgnoreCycles la serialización fallaba con una excepción tipo "ReferenceLoopHandling detected". Ahora simplemente pone null en lugar de la referencia repetida.

¿Cuál es la trampa?

Ventaja: tu código no falla con excepción.

Desventaja: después de deserializar tendrás que restaurar manualmente alguna de las relaciones. Por ejemplo, si un grafo de usuarios se referencia mutuamente (empleado y su jefe), después de deserializar alguna referencia puede quedar vacía.

8. Cómo diseñar estructuras complejas para serialización

Si sabes de antemano que tus objetos forman ciclos, o te importa que al restaurar la estructura todas las relaciones queden exactamente igual, es mejor almacenar no los objetos en sí, sino sus identificadores.

Ejemplo: guardar identificadores en lugar de referencias

Cambiamos la clase Book para que la referencia al autor sea por identificador:

public class Book
{
    public string Title { get; set; }
    public int Year { get; set; }

    public int AuthorId { get; set; }
}

En lugar de lista de libros en el autor, una lista de identificadores de libros. Para restaurar las relaciones después de la deserialización tendrás que "emparejar" por id, pero así evitas ciclos peligrosos.

¿Por qué importa esto en proyectos reales?

  • Bases de datos casi siempre usan identificadores, porque con ellos es más sencillo exportar/importar datos.
  • REST API también suelen intercambiar ids en vez de estructuras profundamente anidadas.

9. Colecciones anidadas: serializar árboles

Un caso frecuente son jerarquías de profundidad arbitraria: árbol de carpetas, estructura de menú, catálogo de productos con subcategorías.

Ejemplo de clase para un "árbol":

public class Folder
{
    public string Name { get; set; }
    public List<Folder> Children { get; set; } = new();
}

Creamos un árbol:

var root = new Folder
{
    Name = "Root",
    Children = new List<Folder>
    {
        new Folder { Name = "Sub1", Children = { new Folder { Name = "Sub1-1" } } },
        new Folder { Name = "Sub2" }
    }
};

Serializamos y mostramos:

string jsonTree = JsonSerializer.Serialize(root, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonTree);

El JSON resultante para ese árbol muestra claramente la jerarquía con arrays anidados.

10. Particularidades de serializar arrays y listas

Si una propiedad es un array (T[]) o una colección (List<T>), la serialización la convertirá en un array JSON normal.

public class Shop
{
    public string Name { get; set; }
    public string[] Departments { get; set; }
}
var shop = new Shop
{
    Name = "Universam",
    Departments = new[] { "Ovoschi", "Frukty", "Myaso" }
};

string jsonShop = JsonSerializer.Serialize(shop, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonShop);

El JSON será algo así:

{
  "Name": "Universam",
  "Departments": [
    "Ovoschi",
    "Frukty",
    "Myaso"
  ]
}

11. Impacto de los atributos en objetos anidados

Si usas [JsonIgnore] en propiedades dentro de objetos anidados, esas propiedades tampoco aparecerán en el JSON final sin importar el nivel de anidamiento.

public class SecretBook : Book
{
    [JsonIgnore]
    public string SecretCode { get; set; }
}

Ese enfoque se usa frecuentemente para proteger información privada: si no necesitas serializar ciertos datos internos, añades el atributo y listo.

12. Consejos prácticos

  • En entrevistas suelen preguntar: "¿Cómo serializar un árbol (Tree)?" y "¿Qué hacer con referencias cíclicas?". Prepara ejemplos con ReferenceHandler.IgnoreCycles y con almacenamiento por identificadores.
  • En proyectos comerciales se serializan pedidos, facturas, usuarios, catálogos, informes complejos. La anidación está por todas partes.
  • Si trabajas con grafos o árboles, intenta evitar ciclos o usa id-keys.
  • Si usas una API externa, acuerda el formato de las estructuras anidadas con antelación para evitar sorpresas al parsear JSON.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION