CodeGym /Cursos /C# SELF /Problema de referências cíclicas

Problema de referências cíclicas

C# SELF
Nível 46 , Lição 3
Disponível

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

2
Tarefa
C# SELF, nível 46, lição 3
Bloqueado
Criação de objetos com referências cíclicas
Criação de objetos com referências cíclicas
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION