System.Text.Json.JsonException:Es wurde ein möglicher Objektzyklus erkannt, der nicht unterstützt wird

System.Text.Json.JsonException:Es wurde ein möglicher Objektzyklus erkannt, der nicht unterstützt wird

Wenn Sie System.Text.Json.JsonSerializer verwenden, um ein Objekt mit einem Zyklus zu serialisieren, erhalten Sie die folgende Ausnahme:

Dies ist das gleiche Problem, über das ich in diesem Artikel über die Objektzyklusausnahme von Newtonsoft geschrieben habe, außer dass in diesem Fall System.Text.Json.JsonSerializer anstelle von Newtonsoft verwendet wird. Die möglichen Lösungen ähneln denen in diesem Artikel, sind aber nicht genau gleich.

Erstens, was ist ein Objektzyklus? Serialisierer arbeiten, indem sie die Eigenschaften eines Objekts rekursiv durchlaufen. Wenn es auf einen Verweis auf ein Objekt trifft, auf das es bereits gestoßen ist, bedeutet dies, dass es einen Zyklus gibt. Der Serializer muss mit diesem Zyklus fertig werden, sonst würde er unendlich rekursieren und schließlich eine StackOverflowException bekommen. Die Standardstrategie von JsonSerializer für den Umgang mit Zyklen besteht darin, eine Ausnahme auszulösen.

Hier ist ein Beispiel für ein Objekt mit einer Zirkelreferenz. Die Child-Klasse bezieht sich auf die Parent-Klasse, die wiederum auf die Child-Klasse verweist:

Parent harry = new Parent()
{
	Name = "Harry"
};
Parent mary = new Parent()
{
	Name = "Mary"
};
harry.Children = new List<Child>()
{
	new Child() { Name = "Barry", Dad=harry, Mom=mary }
};
mary.Children = harry.Children;

var json = JsonSerializer.Serialize(harry, new JsonSerializerOptions() 
{
	WriteIndented = true
});

Console.WriteLine(json);
Code language: C# (cs)

Aufgrund des Zirkelverweises löst der Aufruf von JsonSerializer.Serialize() die JsonException „Objektzyklus erkannt“ aus.

In diesem Artikel zeige ich fünf verschiedene Optionen zur Lösung dieses Problems. Wählen Sie die Option, die in Ihrem spezifischen Szenario am sinnvollsten ist.

Aktualisiert am 18.08.2022, um die neue Option in .NET 6 zu erläutern.

Option 1 – Verwenden Sie das JsonIgnore-Attribut, damit der Serializer die Eigenschaft mit dem Zirkelverweis ignoriert

Setzen Sie das JsonIgnore-Attribut auf die Eigenschaften mit den Zirkelverweisen. Dies weist den Serialisierer an, nicht zu versuchen, diese Eigenschaften zu serialisieren.

public class Child
{
	[System.Text.Json.Serialization.JsonIgnore]
	public Parent Mom { get; set; }
	[System.Text.Json.Serialization.JsonIgnore]
	public Parent Dad { get; set; }
	public string Name { get; set; }
}
Code language: C# (cs)

Das resultierende JSON sieht folgendermaßen aus:

{
	"Children": [{
		"Name": "Barry"
	}],
	"Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

Wenn Sie sich dafür entscheiden, diese Informationen nicht zu serialisieren, hat die andere Seite möglicherweise Probleme mit der Deserialisierung, da die Mom/Dad-Eigenschaften null sind.

Option 2 – Entfernen Sie den Zirkelverweis

Möglicherweise haben Sie diesen Zirkelverweis versehentlich erstellt, oder die Eigenschaft ist für Sie nicht wichtig. In beiden Fällen ist die Lösung einfach:Entfernen Sie die Eigenschaft.

Ausnahmeeigenschaften sind eine häufige Ursache für dieses Problem. In diesem Beispiel habe ich eine Message-Klasse mit einer Exception-Eigenschaft.

public class Message
{
	public string Name { get; set; }
	public Exception Exception { get; set; }
	public void Throw()
	{
		throw new Exception();
	}
}
Code language: C# (cs)

Als Nächstes werde ich eine Ausnahme auslösen, sie auf ein Objekt kleben und versuchen, sie zu serialisieren:

try
{
	var msg = new Message()
	{
		Name = "hello world"
	};
	msg.Throw();
}
catch (Exception ex)
{
	var errorMessage = new Message()
	{
		Name = "Error",
		Exception = ex
	};

	var json = JsonSerializer.Serialize(errorMessage, new JsonSerializerOptions()
	{
		WriteIndented = true
	});

	Console.WriteLine(json);
}
Code language: C# (cs)

Dies führt zu der Zirkelverweis-Ausnahme.

Ich kann es lösen, indem ich die Exception-Eigenschaft entferne. Stattdessen füge ich eine String-Eigenschaft hinzu, die die Ausnahmemeldung enthält.

public class Message
{
	public string Name { get; set; }
	public string ExceptionMessage { get; set; }
	public void Throw()
	{
		throw new Exception();
	}
}
Code language: C# (cs)

Option 3 – Verwenden Sie stattdessen Newtonsoft und verwenden Sie ReferenceLoopHandling.Ignore (vor .NET 6)

In .NET 6 haben sie System.Text.Json.JsonSerializer um eine Option zum Ignorieren von Zirkelbezügen erweitert (siehe Option 6 unten). Wenn Sie eine Version vor .NET 6 verwenden, können Sie dies mit Newtonsoft tun.

Fügen Sie zuerst das Nuget-Paket Newtonsoft.Json hinzu. Dies verwendet die Paket-Manager-Konsole:

 Install-Package Newtonsoft.Json
Code language: PowerShell (powershell)

Verwenden Sie dann JsonConvert.SerializeObject() und übergeben Sie die Option ReferenceLoopHandling.Ignore:

using Newtonsoft.Json;

var json = JsonConvert.SerializeObject(harry, Formatting.Indented,
                    new JsonSerializerSettings()
                    {
                        ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                    });
Code language: C# (cs)

Das resultierende JSON sieht folgendermaßen aus:

{
  "Children": [
    {
      "Mom": {
        "Name": "Mary"
      },
      "Name": "Barry"
    }
  ],
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

Option 4 – Erstellen Sie einen JsonConverter, um anzupassen, wie das problematische Objekt serialisiert wird

Angenommen, Sie möchten dieses Zirkelbezugsproblem lösen, ohne die zu serialisierenden Klassen ändern zu müssen. Dies können sogar Klassen von Drittanbietern sein, die Sie nicht ändern können. In jedem Fall können Sie die Serialisierung jedes Objekts anpassen, indem Sie JsonConverter unterklassen und die Serialisierung für dieses Objekt steuern.

Fügen Sie zuerst eine JsonConverter-Unterklasse wie folgt hinzu:

public class ChildJsonConverter : JsonConverter<Child>
{
	public override bool CanConvert(Type objectType)
	{
		return objectType == typeof(Child);
	}

	public override Child Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		return null; //Doesn't handle deserializing
	}

	public override void Write(Utf8JsonWriter writer, Child value, JsonSerializerOptions options)
	{
		writer.WriteStartObject();
		writer.WriteString(nameof(value.Name), value.Name);
		writer.WriteString(nameof(value.Mom), value.Mom?.Name);
		writer.WriteString(nameof(value.Dad), value.Dad?.Name);
		writer.WriteEndObject();
	}
}
Code language: C# (cs)

Verwenden Sie dann diesen Konverter, indem Sie ihn wie folgt an die JsonSerializerOptions.Converters-Liste übergeben:

var options = new JsonSerializerOptions()
{
	WriteIndented = true
};
options.Converters.Add(new ChildJsonConverter());
var json = JsonSerializer.Serialize(harry, options);
Code language: C# (cs)

Dies gibt das folgende JSON aus:

{
  "Children": [
    {
      "Name": "Barry",
      "Mom": "Mary",
      "Dad": "Harry"
    }
  ],
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

Option 5 – Verwenden Sie die Option ReferenceHandler.Preserve (in .NET 5)

Ab .NET 5 fügten sie JsonSerializerOption die Eigenschaft ReferenceHandler hinzu.

Sie können es wie folgt verwenden:

var json = JsonSerializer.Serialize(harry, new JsonSerializerOptions()
{
	WriteIndented = true,
	ReferenceHandler = ReferenceHandler.Preserve
});
Code language: C# (cs)

Beim Serialisieren werden dem JSON Metadateneigenschaften hinzugefügt. Das sieht also so aus:

{
  "$id": "1",
  "Children": {
    "$id": "2",
    "$values": [
      {
        "$id": "3",
        "Mom": {
          "$id": "4",
          "Children": {
            "$ref": "2"
          },
          "Name": "Mary"
        },
        "Dad": {
          "$ref": "1"
        },
        "Name": "Barry"
      }
    ]
  },
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

Dieses JSON hat Metadateneigenschaften. Solange der Deserializer weiß, wie er mit Metadateneigenschaften umzugehen hat, ist das kein Problem.

Newtonsoft behandelt standardmäßig Metadateneigenschaften, während Sie bei System.Text.Json die ReferenceHandler-Eigenschaft angeben müssen, wenn Sie deserialisieren:

var parent = Newtonsoft.Json.JsonConvert.DeserializeObject<Parent>(json);

var parent2 = JsonSerializer.Deserialize<Parent>(json, new JsonSerializerOptions()
{
	ReferenceHandler = ReferenceHandler.Preserve
});
Code language: C# (cs)

Wenn Sie ReferenceHandler.Preserve hier nicht angeben, erhalten Sie die folgende Ausnahme:

Wenn Sie diese Option verwenden, um mit Zirkelverweisen umzugehen, stellen Sie sicher, dass der Deserializer weiß, wie er mit Metadateneigenschaften angemessen umgeht.

Option 6 – Verwenden Sie die Option ReferenceHandler.IgnoreCycles (in .NET 6)

In .NET 6 haben sie die Option ReferenceHandler.IgnoreCycles zu System.Text.Json hinzugefügt. Dadurch können Sie Zirkelverweise ignorieren.

So verwenden Sie es:

var json = JsonSerializer.Serialize(harry, new JsonSerializerOptions()
{
	WriteIndented = true,
	ReferenceHandler = ReferenceHandler.IgnoreCycles
});
Code language: C# (cs)

Wenn Sie mit dieser Option serialisieren, werden Zirkelverweise auf Null gesetzt. Folgendes wird ausgegeben:

{
  "Children": [
    {
      "Mom": {
        "Children": null,
        "Name": "Mary"
      },
      "Dad": null,
      "Name": "Barry"
    }
  ],
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

Wenn Sie nicht möchten, dass Nullen so angezeigt werden, können Sie alle Null-Eigenschaften mit der DefaultIgnoreCondition-Einstellung ignorieren:

new JsonSerializerOptions()
{
	WriteIndented = true,
	ReferenceHandler = ReferenceHandler.IgnoreCycles,
	DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
}
Code language: C# (cs)