1. Introdução
Imagine um aplicativo para uma livraria: os objetos não são só livros, mas também autores com biografia, editoras que publicam livros, funcionários, seções... Se a serialização só suportasse objetos "planos", nosso app ficaria no nível de uma agenda. Em projetos reais os dados quase sempre são multinível e aninhados. Por isso saber serializar e desserializar estruturas hierárquicas é uma habilidade que separa um desenvolvedor médio de um verdadeiro mestre em serialização .NET.
Vamos ver como os serializadores modernos de C# (no exemplo System.Text.Json) permitem salvar não só árvores de objetos, mas verdadeiras "selvas". E também como projetar classes para serialização corretamente, para não ter que extrair dados manualmente depois.
2. Modelando estruturas hierárquicas
Vamos expandir nosso modelo. Nos exemplos anteriores tínhamos objetos Book, Author e a classe Library que guarda uma lista de livros. Agora adicionamos mais um nível: cada autor terá uma lista de livros que escreveu (sim, isso duplica informação — mais adiante discutimos consequências!), e a biblioteca também terá uma lista de funcionários Employee.
Diagrama de classes
A estrutura de objetos ficará mais ou menos assim:
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
Esse approach nos permite montar um sistema completo de biblioteca. Sim, existe o risco de referências "em loop" (por exemplo, o autor tem livros e o livro tem autor: um serial infinito!). Falaremos disso mais adiante.
3. Exemplo de implementação das classes
Primeiro descrevemos as classes C# do nosso modelo. Já aproveitamos e adicionamos comentários:
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 é potencialmente "perigoso": pode criar uma referência cíclica.
// Mas para a demonstração vamos manter.
public List<Book> Books { get; set; } = new();
}
public class Employee
{
public string Name { get; set; }
public string Position { get; set; }
}
4. Montando a estrutura: criando a biblioteca
Agora vamos criar a biblioteca com livros, autores e funcionários para testar a serialização.
// Criando autores
var author1 = new Author { Name = "Vilyam Golding", BirthYear = 1911 };
var author2 = new Author { Name = "John Updike", BirthYear = 1932 };
var author3 = new Author { Name = "Jerome Salinger", BirthYear = 1919 };
// Criando livros
var book1 = new Book { Title = "O Senhor das Moscas", Year = 1954, Author = author1 };
var book2 = new Book { Title = "Centauro", Year = 1963, Author = author2 };
var book3 = new Book { Title = "O Apanhador no Campo de Centeio", Year = 1951, Author = author3 };
// Adicionando livros aos autores
author1.Books.Add(book1);
author2.Books.Add(book2);
author3.Books.Add(book3);
// Criando funcionários
var emp1 = new Employee { Name = "Ivan Ivanov", Position = "Bibliotecário" };
var emp2 = new Employee { Name = "Sergey Sergeev", Position = "Diretor" };
// Montando a biblioteca
var library = new Library
{
Name = "Biblioteca Municipal",
Books = new List<Book> { book1, book2, book3 },
Employees = new List<Employee> { emp1, emp2 }
};
Claro que em código real você automatizaria a criação dos objetos e suas ligações, para não gerenciar manualmente cada livro/autor. Mas para o exemplo serve.
5. Serializando para JSON
Usamos a abordagem clássica com System.Text.Json:
using System.Text.Json;
// Serializamos a biblioteca para JSON
var options = new JsonSerializerOptions
{
WriteIndented = true, // formato legível com indentação
ReferenceHandler = ReferenceHandler.IgnoreCycles // evitamos loops!
};
string json = JsonSerializer.Serialize(library, options);
// Mostramos o resultado
Console.WriteLine(json);
Ponto interessante: se não usar a opção ReferenceHandler.IgnoreCycles, a serialização pode entrar em loop — já que o autor tem uma lista de livros e o livro tem autor, o serializador poderia percorrer infinitamente (até estourar). A opção IgnoreCycles resolve isso: se durante a travessia o serializador encontra um objeto que já foi serializado mais acima — ele simplesmente escreve null no lugar da serialização repetida.
Como fica o JSON?
{
"Name": "Biblioteca Municipal",
"Books": [
{
"Title": "Centauro",
"Year": 1963,
"Author": {
"Name": "John Updike",
"BirthYear": 1932,
"Books": [
{
"Title": "Centauro",
"Year": 1963,
"Author": null
},
{
"Title": "As Bruxas de Eastwick",
"Year": 1984,
"Author": null
}
]
}
},
{
"Title": "As Bruxas de Eastwick",
"Year": 1984,
"Author": {
"Name": "John Updike",
"BirthYear": 1932,
"Books": [
{
"Title": "Centauro",
"Year": 1963,
"Author": null
},
{
"Title": "As Bruxas de Eastwick",
"Year": 1984,
"Author": null
}
]
}
}
],
"Employees": [
{
"Name": "Ivan Ivanov",
"Position": "Bibliotecário"
},
{
"Name": "Sergey Sergeev",
"Position": "Diretor"
}
]
}
Repare: dentro do array Books o autor das sub-livros já não tem o campo do autor — em vez disso Author: null. Isso é como o ciclo é quebrado na serialização.
6. Desserializando de volta para objetos
Agora desserializamos os dados para objetos novamente:
// Restaurando o objeto do JSON
var libraryCopy = JsonSerializer.Deserialize<Library>(json, options);
Console.WriteLine(libraryCopy.Name); // "Biblioteca Municipal"
Console.WriteLine($"Livros: {libraryCopy.Books.Count}");
Console.WriteLine($"Funcionários: {libraryCopy.Employees.Count}");
Mas atenção! A restauração de referências cíclicas não funciona 100%: nos livros aninhados dentro da lista do autor o campo Author ficará null, porque o serializador cortou a cadeia para evitar aninhamento infinito.
Ponto importante: serializar relacionamentos complexos (por exemplo, pai — filhos — pai) com o serializador padrão sempre envolve um compromisso: ou você perde parte das ligações, ou precisa restaurar manualmente as referências depois da desserialização.
7. Referências cíclicas (aninhamento vs ciclos)
Se na sua estrutura existem caminhos que fazem um objeto referenciar a si mesmo via outros objetos (referência cíclica) — serializadores padrão como System.Text.Json e Newtonsoft.Json, especialmente em modo estrito, lidam com isso de formas diferentes. Antes da opção ReferenceHandler.IgnoreCycles a serialização lançava exceção do tipo "ReferenceLoopHandling detected". Agora ele simplesmente coloca null no lugar da referência repetida.
Qual é a pegadinha?
Pró: seu código não cai com erro.
Contra: após a desserialização você pode ter que restaurar manualmente parte das ligações. Por exemplo, num grafo de usuários que se referenciam (chefe/subordinado) — depois da desserialização alguma referência pode ficar vazia.
8. Como projetar estruturas complexas para serialização
Se você sabe de antemão que seus objetos formarão ciclos, ou é importante que depois de restaurar tudo as referências fiquem exatamente como antes — é melhor armazenar identificadores em vez dos próprios objetos.
Exemplo: guardar ids em vez de referências
Modificamos a classe Book para que a referência para o autor seja por identificador:
public class Book
{
public string Title { get; set; }
public int Year { get; set; }
public int AuthorId { get; set; }
}
Em vez da lista de livros no autor, manteríamos uma lista de ids de livros. Para restaurar as ligações após a desserialização você teria que "casar" por id, mas aí não corre o risco de ciclos perigosos.
Por que isso importa em projetos reais?
- Bancos de dados quase sempre usam identificadores, porque é mais simples trabalhar com exportação/importação.
- REST API também costumam trocar ids, não estruturas aninhadas muito complexas.
9. Coleções aninhadas: serializando árvores
Um caso comum são hierarquias de profundidade arbitrária: árvore de pastas, estrutura de menu, catálogo com subcategorias.
Exemplo de classe para "árvore":
public class Folder
{
public string Name { get; set; }
public List<Folder> Children { get; set; } = new();
}
Criando a árvore:
var root = new Folder
{
Name = "Root",
Children = new List<Folder>
{
new Folder { Name = "Sub1", Children = { new Folder { Name = "Sub1-1" } } },
new Folder { Name = "Sub2" }
}
};
Serializando e mostrando:
string jsonTree = JsonSerializer.Serialize(root, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonTree);
O JSON resultante para essa árvore mostra claramente a hierarquia com arrays aninhados.
10. Particularidades da serialização de arrays e listas
Se uma propriedade for um array (T[]) ou coleção (List<T>), a serialização a transforma em um array JSON comum.
public class Shop
{
public string Name { get; set; }
public string[] Departments { get; set; }
}
var shop = new Shop
{
Name = "Supermercado",
Departments = new[] { "Legumes", "Frutas", "Carnes" }
};
string jsonShop = JsonSerializer.Serialize(shop, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonShop);
O JSON ficará mais ou menos assim:
{
"Name": "Supermercado",
"Departments": [
"Legumes",
"Frutas",
"Carnes"
]
}
11. Efeito de atributos sobre objetos aninhados
Se usar [JsonIgnore] em propriedades de objetos aninhados, elas também não entrarão no JSON final independentemente do nível de aninhamento.
public class SecretBook : Book
{
[JsonIgnore]
public string SecretCode { get; set; }
}
Esse approach é frequentemente usado para proteger dados privados: se não precisa serializar certos campos internos, adiciona o atributo — e pronto, os dados ficam fora do JSON.
12. Dicas práticas
- Em entrevistas costumam perguntar: "Como serializar uma árvore (Tree)?" e "O que fazer com referências cíclicas?". Tenha exemplos com ReferenceHandler.IgnoreCycles e com armazenamento por identificadores.
- Em projetos comerciais serializam-se pedidos, faturas, usuários, catálogos de produtos, relatórios complexos. Aninhamento é comum.
- Se trabalhar com grafos ou árvores, evite ciclos quando possível ou use ids.
- Se consumir uma API externa, combine o formato das estruturas aninhadas antecipadamente para evitar surpresas no parsing do JSON.
GO TO FULL VERSION