1. Einleitung
Ein zyklischer (oder kreisförmiger) Verweis entsteht, wenn ein Objekt direkt oder indirekt eine Referenz auf ein anderes Objekt enthält, das am Ende wieder auf das erste zurückverweist.
Ein reales Beispiel
Lassen Sie uns ein lebensnahes Beispiel für unsere Buchbibliothek machen. Angenommen, wir haben die Klasse Book, die die Eigenschaft Author hat, und die Klasse Author hat die Eigenschaft Books vom Typ List<Book>, damit er all seine Bücher kennt.
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; }
}
Wenn wir nun einen Autor und ein Buch erstellen und die Verknüpfungen setzen, entsteht ein "geschlossener Kreis":
var author = new Author { Name = "Marcel Proust", BirthYear = 1871 };
var book = new Book { Title = "Swann's Way", Author = author };
author.Books.Add(book);
// Fertig, jetzt verweist author auf book und book verweist auf author!
Warum ist das ein Problem?
Wenn man so ein Objekt in JSON serialisiert, geht der Serializer die Eigenschaften entlang. Er sieht, dass der Autor Bücher hat, darin wieder der Autor... der wieder Bücher enthält... und so unendlich weiter.
author -> books[] -> author -> books[] ...
Das ist wie in einen Spiegel vor einem anderen Spiegel zu schauen — die Reflexionen laufen ins Unendliche. Nur statt hübscher Reflexionen bekommt man einen Stackoverflow (StackOverflowException).
2. Wie reagiert der Serializer auf zyklische Verweise?
Serialisierungsfehler
Standardmäßig kann System.Text.Json Zyklen nicht verarbeiten. Wenn Sie versuchen, so eine Struktur zu serialisieren, bekommen Sie die Ausnahme JsonException: "A possible object cycle was detected".
Beispiel, das den Fehler auslöst:
string json = JsonSerializer.Serialize(author); // BÄM! JsonException
Visualisierung:
graph TD;
Author --> Book;
Book --> Author;
3. Wie löst man das Problem zyklischer Verweise?
Wir betrachten mehrere praxisnahe Ansätze, jeder mit seinen Vor- und Nachteilen. Wie Sie sich sicher denken können, gibt es kein universelles "magisches Flag" (leider).
Zyklen vor der Serialisierung entfernen
Das Einfachste ist: so etwas erst gar nicht machen. Vor der Serialisierung Referenzen, die zum Zyklus führen, auf null setzen (oder ignorieren).
Wie das in der Praxis aussieht:
// Vorübergehend die Referenz des Autors auf die Bücher entfernen
var authorToSerialize = new Author
{
Name = author.Name,
BirthYear = author.BirthYear,
Books = null // oder das Property ganz weglassen
};
string json = JsonSerializer.Serialize(authorToSerialize);
// Jetzt wurde alles ohne Probleme serialisiert!
Vorteile: einfach, schnell, verständlich.
Nachteile: du verlierst Teile der Daten (nach der Deserialisierung sind die Rückverweise weg).
Das Attribut [JsonIgnore] verwenden
Man kann die Property, die am zyklischen Verweis beteiligt ist, als ignoriert markieren:
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
[JsonIgnore]
public List<Book> Books { get; set; }
}
Beim Serialisieren des Autors werden seine Bücher dann nicht im JSON erscheinen. Das ist ähnlich wie der Ansatz oben, aber deklarativ und ohne manuelle "Bereinigung".
Vorteile: einfacher, geringere Gefahr, die Bereinigung zu vergessen.
Nachteile: Informationen über die Bücher des Autors gehen im JSON verloren.
Identifiers statt verschachtelter Objekte verwenden
Wenn es wichtig ist, beide Seiten der Beziehung (Autor und Bücher) zu behalten, man aber keine Zyklen wollen, dann verwende eindeutige Identifiers statt verschachtelter Objekte:
public class Book
{
public string Title { get; set; }
public int AuthorId { get; set; } // statt Author
}
public class Author
{
public int AuthorId { get; set; }
public string Name { get; set; }
// wir speichern die Bücher nicht oder speichern nur ihre Ids
}
Im JSON sind dann keine Objekte verschachtelt, sondern nur Identifiers. Das ist ein verbreiteter Ansatz in DBs, REST APIs und Systemen mit eindeutigen Referenzen.
Vorteile: keine Zyklen, kompaktes JSON, Verknüpfungen lassen sich über Id wiederherstellen.
Nachteile: die gewohnte Objektmodellierung leidet, bei der Deserialisierung ist eine Suche per Id nötig.
Mini-Vergleichstabelle:
| Ansatz | Problem mit Zyklen gelöst? | Datenverlust? | Anwendbarkeit |
|---|---|---|---|
| [JsonIgnore] | Ja | Ja | Wenn Verschachtelung nicht kritisch ist |
| Referenz manuell entfernen | Ja | Ja | Schnell vor der Serialisierung |
| Ids statt Objekt speichern | Ja | Nein* | REST, Datenbanken, komplexe Systeme |
* Die Daten sind nicht verloren, aber nicht sofort verfügbar (man muss per Id nachschlagen).
4. Wie bringt man System.Text.Json bei, zyklische Verweise zu serialisieren?
Ab .NET 5 gibt es in JsonSerializerOptions einen Referenzmodus: options.ReferenceHandler = ReferenceHandler.Preserve.
Dieser Modus verwendet spezielle Felder $id und $ref für wiederkehrende Objekte.
Beispiel
var options = new JsonSerializerOptions
{
WriteIndented = true,
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve
};
string json = JsonSerializer.Serialize(author, options);
Console.WriteLine(json);
Das resultierende JSON sieht so aus:
{
"$id": "1",
"Name": "Marcel Proust",
"BirthYear": 1871,
"Books": {
"$id": "2",
"$values": [
{
"$id": "3",
"Title": "Swann's Way",
"Author": {
"$ref": "1"
}
}
]
}
}
- $id — eindeutige Kennung eines Objekts im JSON
- $ref — Referenz auf ein bereits serialisiertes Objekt
Bei der Deserialisierung wird alles korrekt wiederhergestellt (ohne endlose Zyklen oder Stackfehler).
Eigenschaften und Einschränkungen
- So ein JSON ist ungewöhnlich für Frontends: die meisten JS-Clients verstehen $id/$ref nicht ohne zusätzliche Logik.
- Die JSON-Größe wächst, es ist schwerer, es per Auge zu debuggen.
- Funktioniert nur, wenn man explizit ReferenceHandler.Preserve einschaltet.
- Gilt nicht für Value-Typen (dort können keine Zyklen existieren).
Wie deserialisiert man so ein JSON?
Genau wie gewöhnliches JSON, aber mit denselben JsonSerializerOptions:
var deserializedAuthor = JsonSerializer.Deserialize<Author>(json, options);
5. Und was ist mit Newtonsoft.Json (Json.NET)?
Historisch konnte Newtonsoft.Json schon früher mit Zyklen umgehen als System.Text.Json. Dafür gibt es das Attribut [JsonObject(IsReference = true)] und globale Serialisierungseinstellungen.
Attribute für Referenzen
[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; }
}
Dann serialisiert man so:
var settings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
Formatting = Formatting.Indented
};
string json = JsonConvert.SerializeObject(author, settings);
Am Ende erhält man JSON mit $id und $ref, ähnlich dem ReferenceHandler.Preserve-Modus.
Kurzes Fazit
- Wenn der Austausch zwischen .NET-Anwendungen stattfindet — schaltet referenzielle Serialisierung ein (ReferenceHandler.Preserve oder PreserveReferencesHandling).
- Wenn die Daten an JavaScript/andere Clients gehen — bricht die Zyklen auf: [JsonIgnore], Referenzen bereinigen oder auf Ids umstellen.
6. Wie man Fehler und Ärger vermeidet
Sehr oft stolpern Anfänger (und selbst Erfahrene) über Abstürze des Serializers wegen Zyklen. Denkt dran: wenn Collections/Properties sich gegenseitig referenzieren — überprüft das Architekturmodell.
Habt keine Scheu davor, [JsonIgnore] für Properties zu nutzen, die für den externen Austausch nicht nötig sind.
Die klassische Falle ist die Serialisierung von Many-to-Many-Beziehungen (z.B. Studenten ↔ Kurse). Ohne Aufbrechen der Zyklen oder referenzielle Serialisierung funktioniert das nicht.
In REST APIs schickt man oft das Objekt "in eine Richtung": z.B. das Buch kennt den Autor, und der Autor kennt nur die Id der Bücher (oder weiß im Vertrag gar nichts von Büchern).
GO TO FULL VERSION