JavaRush /Курсы /C# SELF /Проблема циклических ссылок

Проблема циклических ссылок

C# SELF
46 уровень , 3 лекция
Открыта

1. Введение

Циклическая (или круговая) ссылка возникает, когда один объект прямо или косвенно содержит ссылку на другой объект, который в итоге ссылается обратно на первый.

Реальный пример

Давайте попробуем сделать жизненный пример для нашей книжной библиотеки. Пусть у нас есть класс Book, у которого есть свойство Author, а у класса Author есть свойство Books типа List<Book>, чтобы он помнил все свои книги.


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

Теперь, если мы создадим одного автора и одну книгу, установим связи, получим "замкнутый круг":


var author = new Author { Name = "Марсель Пруст", BirthYear = 1871 };
var book = new Book { Title = "В сторону Свана", Author = author };

author.Books.Add(book);

// Всё, теперь author ссылается на book, а book ссылается на author!

Почему это проблема?

Когда вы сериализуете такой объект в JSON, сериализатор начинает идти по свойствам. Он видит, что у автора есть книги, внутри которых снова автор... который снова содержит книги... которые снова содержат авторов... и так до бесконечности.

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

Это как смотреть в зеркало, стоя перед другим зеркалом — отражения уходят в бесконечность. Только вместо красивых отражений — переполнение стека (StackOverflowException).

2. Как сериализатор реагирует на циклические ссылки?

Ошибка сериализации

По умолчанию System.Text.Json не умеет обрабатывать циклические ссылки. Если вы попробуете сериализовать такую структуру, получите исключение JsonException: "A possible object cycle was detected".

Пример, который вызовет ошибку:


string json = JsonSerializer.Serialize(author); // БАЦ! JsonException

Визуализация:

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

3. Как решать проблему циклических ссылок?

Рассмотрим несколько реальных подходов, каждый со своими плюсами и минусами. Как вы уже догадались, универсального "волшебного флага" нет (а жаль).

Удалять циклы перед сериализацией

Самое простое – не делать так. Перед сериализацией обнулять (или игнорировать) ссылки, ведущие к циклу.

Как это выглядит на практике:


// Временно уберём ссылку у автора на книги
var authorToSerialize = new Author
{
    Name = author.Name,
    BirthYear = author.BirthYear,
    Books = null // или можно вообще не включать свойство
};

string json = JsonSerializer.Serialize(authorToSerialize);
// Теперь всё сериализовалось без проблем!

Плюсы: просто, быстро, понятно.
Минусы: вы теряете часть данных (после десериализации не узнаете обратные связи).

Использовать атрибут [JsonIgnore]

Можно пометить свойство, участвующее в круговой ссылке, как игнорируемое:


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

Теперь при сериализации автора его книги сохраняться не будут. Это похоже на способ выше, но декларативно и без ручной "чистки".

Плюсы: проще, меньше риска забыть очистить ссылку.
Минусы: информация о книгах автора потеряна в JSON.

Использовать идентификаторы вместо вложенных объектов

Если важно сохранить обе стороны связи (и авторов, и книги), но не хочется круговых ссылок, используйте уникальные идентификаторы вместо вложенных объектов:


public class Book
{
    public string Title { get; set; }
    public int AuthorId { get; set; } // вместо Author
}
public class Author 
{
    public int AuthorId { get; set; }
    public string Name { get; set; }
    // не храним книги или храним список их Id
}

В JSON теперь не объекты, а только идентификаторы. Это распространённый подход в БД, REST API и системах с однозначными ссылками.

Плюсы: нет циклов, JSON компактный, ссылки можно восстановить по Id.
Минусы: ломается привычная объектная модель, на десериализации нужен поиск по Id.

Мини-таблица сравнения:

Подход Проблема циклов решена? Потеря данных? Применимость
[JsonIgnore] Да Да Когда вложенность не критична
Удалять ссылку вручную Да Да Быстро перед сериализацией
Хранить Id вместо объекта Да Нет* REST, базы, сложные системы

* Данные не потеряны, но не сразу доступны (нужен поиск по Id).

4. Как научить System.Text.Json сериализовывать циклические ссылки?

Начиная с .NET 5 у JsonSerializerOptions появился режим ссылок: options.ReferenceHandler = ReferenceHandler.Preserve.

Этот режим использует специальные поля $id и $ref для повторных объектов.

Пример


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

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

Получившийся JSON будет выглядеть так:

{
  "$id": "1",
  "Name": "Марсель Пруст",
  "BirthYear": 1871,
  "Books": {
    "$id": "2",
    "$values": [
      {
        "$id": "3",
        "Title": "В сторону Свана",
        "Author": {
          "$ref": "1"
        }
      }
    ]
  }
}
  • $id — уникальный идентификатор объекта в JSON
  • $ref — ссылка на уже сериализованный объект

На десериализации все восстановится корректно (без бесконечных циклов и ошибок стека).

Особенности и ограничения

  • Такой JSON необычен для фронтенда: большинство JS-клиентов не понимают $id/$ref без доп. логики.
  • Размер JSON больше, сложнее дебажить глазами.
  • Работает только при явном включении ReferenceHandler.Preserve.
  • Не относится к типам-значениям (циклов там быть не может).

Как десериализовать такой JSON?

Точно так же, как и обычный, но используйте те же JsonSerializerOptions:


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

5. А что с Newtonsoft.Json (Json.NET)?

Исторически Newtonsoft.Json умел работать с циклами раньше System.Text.Json. Для этого есть атрибут [JsonObject(IsReference = true)] и глобальные настройки сериализации.

Атрибуты для ссылок


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

Далее сериализуем так:


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

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

В итоге получим JSON с $id и $ref, аналогично режиму ReferenceHandler.Preserve.

Быстрый вывод

  • Если обмен между .NET-приложениями — включайте ссылочную сериализацию (ReferenceHandler.Preserve или PreserveReferencesHandling).
  • Если данные идут в JavaScript/иные клиенты — разрывайте циклы: [JsonIgnore], очистка ссылок или переход на Id.

6. Как избежать ошибок и головной боли

Очень часто новички (и даже опытные) сталкиваются с падением сериализатора из‑за циклов. Помните: если коллекции/свойства указывают друг на друга — проверьте архитектуру модели.

Не стесняйтесь использовать [JsonIgnore] для свойств, которые не нужны для внешнего обмена.

Классическая ловушка — сериализация связей «многие ко многим» (например, студенты ↔ курсы). Без разрыва циклов или ссылочной сериализации это не работает.

В REST API чаще отправляют объект «в одну сторону»: например, книга знает автора, а автор — только Id книг (или вовсе не знает о книгах в этом контракте).

2
Задача
C# SELF, 46 уровень, 3 лекция
Недоступна
Создание объектов с циклическими ссылками
Создание объектов с циклическими ссылками
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Дмитрий Уровень 61
15 декабря 2025
Задача по проблеме сериализации циклических ссылок: чтобы избежать проблем с сериализацией - не делай сериализацию.