1. Introdução
Uma referência cíclica (ou circular) acontece quando um objeto contém direta ou indiretamente uma referência para outro objeto que, no fim, referencia de volta o primeiro.
Exemplo real
Vamos montar um exemplo real para nossa biblioteca de livros. Suponha que temos a classe Book, que tem a propriedade Author, e a classe Author tem a propriedade Books do tipo List<Book>, para guardar todos os seus livros.
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
public List<Book> Books { get; set; } = new List<Book>();
}
public class Book
{
public string Title { get; set; }
public Author Author { get; set; }
}
Agora, se criarmos um autor e um livro, estabelecer as ligações resulta num "círculo fechado":
var author = new Author { Name = "Marsel Prust", BirthYear = 1871 };
var book = new Book { Title = "À sombra de Swann", Author = author };
author.Books.Add(book);
// Pronto, agora author referencia book, e book referencia author!
Por que isso é um problema?
Quando você serializa esse objeto para JSON, o serializador começa a percorrer as propriedades. Ele vê que o autor tem livros, dentro dos livros tem de novo o autor... que de novo contém livros... que de novo contêm autores... e assim por diante até o infinito.
author -> books[] -> author -> books[] ...
É como olhar num espelho de frente para outro espelho — os reflexos vão até o infinito. Só que em vez de reflexos bonitos você acaba com estouro de pilha (StackOverflowException).
2. Como o serializador reage a referências cíclicas?
Erro de serialização
Por padrão System.Text.Json não lida com referências cíclicas. Se você tentar serializar essa estrutura, vai receber a exceção JsonException: "A possible object cycle was detected".
Exemplo que vai causar erro:
string json = JsonSerializer.Serialize(author); // PÁ! JsonException
Visualização:
graph TD;
Author --> Book;
Book --> Author;
3. Como resolver o problema de referências cíclicas?
Vamos ver algumas abordagens reais, cada uma com seus prós e contras. Como você já deve imaginar, não existe um "flag mágico" universal (pena).
Remover ciclos antes da serialização
O mais simples — não fazer o ciclo. Antes de serializar, zere (ou ignore) as referências que causam o ciclo.
Como fica na prática:
// Temporariamente removemos a referência do autor para os livros
var authorToSerialize = new Author
{
Name = author.Name,
BirthYear = author.BirthYear,
Books = null // ou simplesmente não incluir a propriedade
};
string json = JsonSerializer.Serialize(authorToSerialize);
// Agora serializou sem problemas!
Prós: simples, rápido, óbvio.
Contras: você perde parte dos dados (depois da desserialização não terá as referências de volta).
Usar o atributo [JsonIgnore]
Você pode marcar a propriedade que participa do ciclo como ignorada:
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
[JsonIgnore]
public List<Book> Books { get; set; }
}
Agora, ao serializar o autor, os livros não serão salvos. É parecido com a abordagem acima, mas declarativo e sem limpeza manual.
Prós: mais simples, menos chance de esquecer de limpar a referência.
Contras: a informação sobre os livros do autor se perde no JSON.
Usar identificadores em vez de objetos aninhados
Se for importante manter os dois lados da relação (autores e livros), mas você não quer ciclos, use identificadores únicos em vez de objetos aninhados:
public class Book
{
public string Title { get; set; }
public int AuthorId { get; set; } // em vez de Author
}
public class Author
{
public int AuthorId { get; set; }
public string Name { get; set; }
// não guarda os Books, ou guarda uma lista de seus Ids
}
No JSON agora estarão os ids, não os objetos. É uma abordagem comum em DBs, REST APIs e sistemas com referências unívocas.
Prós: sem ciclos, JSON mais compacto, referências podem ser reconstruídas via Id.
Contras: quebra o modelo orientado a objetos habitual, na desserialização é necessário buscar por Id.
Mini-tabela comparativa:
| Abordagem | Resolve ciclos? | Perda de dados? | Aplicabilidade |
|---|---|---|---|
| [JsonIgnore] | Sim | Sim | Quando aninhamento não é crítico |
| Remover referência manualmente | Sim | Sim | Rápido antes da serialização |
| Guardar Id em vez do objeto | Sim | Não* | REST, bancos, sistemas complexos |
* Os dados não são perdidos, mas não estão imediatamente disponíveis (é preciso buscar por Id).
4. Como ensinar o System.Text.Json a serializar referências cíclicas?
A partir do .NET 5 o JsonSerializerOptions ganhou um modo de referência: options.ReferenceHandler = ReferenceHandler.Preserve.
Esse modo usa campos especiais $id e $ref para objetos repetidos.
Exemplo
var options = new JsonSerializerOptions
{
WriteIndented = true,
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve
};
string json = JsonSerializer.Serialize(author, options);
Console.WriteLine(json);
O JSON resultante ficará assim:
{
"$id": "1",
"Name": "Marsel Prust",
"BirthYear": 1871,
"Books": {
"$id": "2",
"$values": [
{
"$id": "3",
"Title": "À sombra de Swann",
"Author": {
"$ref": "1"
}
}
]
}
}
- $id — identificador único do objeto no JSON
- $ref — referência a um objeto já serializado
Na desserialização tudo será restaurado corretamente (sem ciclos infinitos e sem erro de pilha).
Particularidades e limitações
- Esse JSON é incomum para frontend: a maioria dos clientes JS não entende $id/$ref sem lógica adicional.
- O tamanho do JSON aumenta, fica mais difícil debugar visualmente.
- Só funciona se você explicitamente ativar ReferenceHandler.Preserve.
- Não se aplica a tipos-valor (lá não existem ciclos).
Como desserializar esse JSON?
Do mesmo jeito que o normal, mas usando as mesmas JsonSerializerOptions:
var deserializedAuthor = JsonSerializer.Deserialize<Author>(json, options);
5. E o Newtonsoft.Json (Json.NET)?
Historicamente Newtonsoft.Json já lidava com ciclos antes do System.Text.Json. Para isso existe o atributo [JsonObject(IsReference = true)] e configurações globais de serialização.
Atributos para referências
[JsonObject(IsReference = true)]
public class Author
{
public string Name { get; set; }
public List<Book> Books { get; set; }
}
[JsonObject(IsReference = true)]
public class Book
{
public string Title { get; set; }
public Author Author { get; set; }
}
Depois serializamos assim:
var settings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
Formatting = Formatting.Indented
};
string json = JsonConvert.SerializeObject(author, settings);
No final teremos JSON com $id e $ref, similar ao modo ReferenceHandler.Preserve.
Conclusão rápida
- Se o intercâmbio é entre aplicações .NET — habilite serialização por referência (ReferenceHandler.Preserve ou PreserveReferencesHandling).
- Se os dados vão para JavaScript/outros clientes — quebre os ciclos: [JsonIgnore], limpar referências ou usar Id.
6. Como evitar erros e dor de cabeça
Muito comum iniciantes (e até gente experiente) terem falha do serializador por causa de ciclos. Lembre-se: se coleções/propriedades apontam umas para as outras — revise a arquitetura do seu modelo.
Não tenha vergonha de usar [JsonIgnore] para propriedades que não fazem parte do contrato externo.
Uma armadilha clássica é serializar relações "muitos-para-muitos" (por exemplo, estudantes ↔ cursos). Sem quebrar ciclos ou usar serialização por referência isso não funciona.
Em REST APIs geralmente envia-se o objeto "num único sentido": por exemplo, o livro conhece o autor, e o autor só tem os Id dos livros (ou nem sabe dos livros nesse contrato).
GO TO FULL VERSION