CodeGym /Courses /C# SELF /The problem of cyclic references

The problem of cyclic references

C# SELF
Level 46 , Lesson 3
Available

1. Introduction

A cyclic (or circular) reference occurs when one object directly or indirectly contains a reference to another object which eventually refers back to the first.

Real-world example

Let's make a real-life example for our book library. Say we have a class Book with a property Author, and the Author class has a property Books of type List<Book> so it remembers all its books.


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; }
}

Now, if we create one author and one book, set up the links, we'll get a "closed loop":


var author = new Author { Name = "Marcel Proust", BirthYear = 1871 };
var book = new Book { Title = "In the Shadow of Swann", Author = author };

author.Books.Add(book);

// That's it — now author references book, and book references author!

Why is this a problem?

When you serialize such an object to JSON, the serializer walks the properties. It sees that the author has books, inside which there's the author again... which contains books again... which contain authors again... and so on to infinity.

author -> books[] -> author -> books[] ...

It's like standing between two mirrors — reflections go on forever. Except instead of nice reflections you get a stack overflow (StackOverflowException).

2. How does the serializer react to cyclic references?

Serialization error

By default System.Text.Json can't handle cyclic references. If you try to serialize such a structure you'll get a JsonException: "A possible object cycle was detected".

Example that will throw:


string json = JsonSerializer.Serialize(author); // BAM! JsonException

Visualization:

graph TD;
    Author --> Book;
    Book --> Author;

3. How to solve cyclic reference issues?

Let's consider several practical approaches, each with pros and cons. As you guessed, there's no universal "magic flag" (sadly).

Remove cycles before serialization

The simplest is: don't create them. Before serialization null out (or omit) the references that form the cycle.

What this looks like in practice:


// Temporarily remove the reference from author to books
var authorToSerialize = new Author
{
    Name = author.Name,
    BirthYear = author.BirthYear,
    Books = null // or omit the property entirely
};

string json = JsonSerializer.Serialize(authorToSerialize);
// Now everything serialized fine!

Pros: simple, fast, easy to understand.
Cons: you lose part of the data (after deserialization you won't get the back-references).

Use the [JsonIgnore] attribute

You can mark the property participating in the cycle as ignored:


public class Author
{
    public string Name { get; set; }
    public int BirthYear { get; set; }
    
    [JsonIgnore] 
    public List<Book> Books { get; set; }
}

Now when serializing the author, its books won't be included. This is like the previous approach but declarative and without manual "cleanup".

Pros: simpler, less risk of forgetting to clear references.
Cons: information about the author's books is lost in JSON.

Use identifiers instead of nested objects

If it's important to keep both sides (authors and books) but you don't want cycles, use unique identifiers instead of nested objects:


public class Book
{
    public string Title { get; set; }
    public int AuthorId { get; set; } // instead of Author
}
public class Author 
{
    public int AuthorId { get; set; }
    public string Name { get; set; }
    // don't store books or store their Ids
}

In JSON you'll have identifiers rather than objects. This is a common approach in databases, REST APIs, and systems with unambiguous links.

Pros: no cycles, compact JSON, links can be restored by Id.
Cons: breaks the familiar object model, deserialization requires lookup by Id.

Mini comparison table:

Approach Cycle problem solved? Data lost? Applicability
[JsonIgnore] Yes Yes When nesting isn't critical
Remove reference manually Yes Yes Quick before serialization
Store Id instead of object Yes No* REST, DBs, complex systems

* Data isn't lost, but not immediately available (requires lookup by Id).

4. How to teach System.Text.Json to serialize cyclic references?

Starting with .NET 5 JsonSerializerOptions got a reference mode: options.ReferenceHandler = ReferenceHandler.Preserve.

This mode uses special fields $id and $ref for repeated objects.

Example


var options = new JsonSerializerOptions
{
    WriteIndented = true,
    ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve
};

string json = JsonSerializer.Serialize(author, options);
Console.WriteLine(json);

The resulting JSON will look like this:

{
  "$id": "1",
  "Name": "Marcel Proust",
  "BirthYear": 1871,
  "Books": {
    "$id": "2",
    "$values": [
      {
        "$id": "3",
        "Title": "In the Shadow of Swann",
        "Author": {
          "$ref": "1"
        }
      }
    ]
  }
}
  • $id — unique identifier of the object in JSON
  • $ref — reference to an already serialized object

On deserialization everything will be restored correctly (no infinite loops or stack errors).

Characteristics and limitations

  • This JSON is unusual for frontends: most JS clients don't understand $id/$ref without extra logic.
  • The JSON size is larger and it's harder to debug by eye.
  • Works only when you explicitly enable ReferenceHandler.Preserve.
  • Does not apply to value types (you can't have cycles there).

How to deserialize such JSON?

Exactly like normal JSON, but use the same JsonSerializerOptions:


var deserializedAuthor = JsonSerializer.Deserialize<Author>(json, options);

5. What about Newtonsoft.Json (Json.NET)?

Historically Newtonsoft.Json handled cycles before System.Text.Json. It provides the attribute [JsonObject(IsReference = true)] and global serializer settings.

Attributes for references


[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; }
}

Then serialize like this:


var settings = new JsonSerializerSettings
{
    PreserveReferencesHandling = PreserveReferencesHandling.Objects,
    Formatting = Formatting.Indented
};

string json = JsonConvert.SerializeObject(author, settings);

You will get JSON with $id and $ref, similar to ReferenceHandler.Preserve.

Quick takeaway

  • If you're exchanging data between .NET apps — enable reference serialization (ReferenceHandler.Preserve or PreserveReferencesHandling).
  • If the data goes to JavaScript/other clients — break the cycles: [JsonIgnore], clear references, or switch to Ids.

6. How to avoid errors and headaches

Beginners (and even experienced devs) often hit serializer failures due to cycles. Remember: if collections/properties point to each other — review your model architecture.

Don't hesitate to use [JsonIgnore] for properties that aren't needed for external exchange.

A classic trap is serializing many-to-many relationships (e.g., students ↔ courses). Without breaking cycles or using reference serialization this won't work.

In REST APIs it's common to send the object "one-way": for example, a book knows its author, and the author only has an Id of books (or doesn't know about books at all in that contract).

2
Task
C# SELF, level 46, lesson 3
Locked
Creating Objects with Circular References
Creating Objects with Circular References
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION