Beibehalten von Verweisen und Behandeln oder Ignorieren von Zirkelbezügen in System.Text.Json
In diesem Artikel wird gezeigt, wie Sie beim Serialisieren und Deserialisieren von JSON in .NET mithilfe von System.Text.Json Verweise beibehalten und Zirkelbezüge behandeln oder ignorieren
Beibehalten von Verweisen und Behandeln von Zirkelbezügen
Um Verweise beizubehalten und Zirkelbezüge zu behandeln, legen Sie ReferenceHandler auf Preserve fest. Diese Einstellung bewirkt folgendes Verhalten:
Beim Serialisieren:
Beim Schreiben komplexer Typen schreibt das Serialisierungsmodul auch Metadateneigenschaften (
$id
,$values
und$ref
).Beim Deserialisieren:
Metadaten werden erwartet (obwohl nicht zwingend erforderlich), und der Deserialisierer versucht, sie zu verstehen.
Der folgende Code veranschaulicht die Verwendung der Einstellung Preserve
.
using System.Text.Json;
using System.Text.Json.Serialization;
namespace PreserveReferences
{
public class Employee
{
public string? Name { get; set; }
public Employee? Manager { get; set; }
public List<Employee>? DirectReports { get; set; }
}
public class Program
{
public static void Main()
{
Employee tyler = new()
{
Name = "Tyler Stein"
};
Employee adrian = new()
{
Name = "Adrian King"
};
tyler.DirectReports = [adrian];
adrian.Manager = tyler;
JsonSerializerOptions options = new()
{
ReferenceHandler = ReferenceHandler.Preserve,
WriteIndented = true
};
string tylerJson = JsonSerializer.Serialize(tyler, options);
Console.WriteLine($"Tyler serialized:\n{tylerJson}");
Employee? tylerDeserialized =
JsonSerializer.Deserialize<Employee>(tylerJson, options);
Console.WriteLine(
"Tyler is manager of Tyler's first direct report: ");
Console.WriteLine(
tylerDeserialized?.DirectReports?[0].Manager == tylerDeserialized);
}
}
}
// Produces output like the following example:
//
//Tyler serialized:
//{
// "$id": "1",
// "Name": "Tyler Stein",
// "Manager": null,
// "DirectReports": {
// "$id": "2",
// "$values": [
// {
// "$id": "3",
// "Name": "Adrian King",
// "Manager": {
// "$ref": "1"
// },
// "DirectReports": null
// }
// ]
// }
//}
//Tyler is manager of Tyler's first direct report:
//True
Imports System.Text.Json
Imports System.Text.Json.Serialization
Namespace PreserveReferences
Public Class Employee
Public Property Name As String
Public Property Manager As Employee
Public Property DirectReports As List(Of Employee)
End Class
Public NotInheritable Class Program
Public Shared Sub Main()
Dim tyler As New Employee
Dim adrian As New Employee
tyler.DirectReports = New List(Of Employee) From {
adrian}
adrian.Manager = tyler
Dim options As New JsonSerializerOptions With {
.ReferenceHandler = ReferenceHandler.Preserve,
.WriteIndented = True
}
Dim tylerJson As String = JsonSerializer.Serialize(tyler, options)
Console.WriteLine($"Tyler serialized:{tylerJson}")
Dim tylerDeserialized As Employee = JsonSerializer.Deserialize(Of Employee)(tylerJson, options)
Console.WriteLine(
"Tyler is manager of Tyler's first direct report: ")
Console.WriteLine(
tylerDeserialized.DirectReports(0).Manager Is tylerDeserialized)
End Sub
End Class
End Namespace
' Produces output like the following example:
'
'Tyler serialized:
'{
' "$id": "1",
' "Name": "Tyler Stein",
' "Manager": null,
' "DirectReports": {
' "$id": "2",
' "$values": [
' {
' "$id": "3",
' "Name": "Adrian King",
' "Manager": {
' "$ref": "1"
' },
' "DirectReports": null
' }
' ]
' }
'}
'Tyler is manager of Tyler's first direct report:
'True
Dieses Feature kann nicht verwendet werden, um Wert- oder unveränderliche Typen beizubehalten. Bei der Deserialisierung wird die Instanz eines unveränderlichen Typs erstellt, nachdem die gesamten Nutzdaten gelesen wurden. Daher wäre es unmöglich, dieselbe Instanz zu deserialisieren, wenn ein Verweis darauf in den JSON-Nutzdaten vorhanden ist.
Für Werttypen, unveränderliche Typen und Arrays werden keine Verweismetadaten serialisiert. Bei der Deserialisierung wird eine Ausnahme ausgelöst, wenn $ref
oder $id
gefunden wird. Wertetypen ignorieren jedoch $id
(und $values
im Falle von Sammlungen), um die Deserialisierung von Payloads zu ermöglichen, die mit Newtonsoft.Json serialisiert wurden, was die Serialisierung von Metadaten für solche Typen ermöglicht.
Um festzustellen, ob Objekte gleich sind, wird ReferenceEqualityComparer.Instance von System.Text.Json verwendet. Dabei wird beim Vergleich zweier Objektinstanzen die Verweisgleichheit (Object.ReferenceEquals(Object, Object)) anstelle der Wertgleichheit (Object.Equals(Object)) verwendet.
Weitere Informationen zur Serialisierung und Deserialisierung von Verweisen finden Sie unter ReferenceHandler.Preserve.
Die ReferenceResolver-Klasse definiert das Verhalten bei Beibehaltung von Verweisen für Serialisierung und Deserialisierung. Erstellen Sie eine abgeleitete Klasse, um benutzerdefiniertes Verhalten anzugeben. Ein Beispiel finden Sie unter GuidReferenceResolver.
Beibehalten von Verweismetadaten über mehrere Serialisierungs- und Deserialisierungsaufrufe hinweg
Standardmäßig werden Verweisdaten nur für jeden Aufruf von Serialize oder Deserializezwischengespeichert. Um Referenzen von einem Serialize
- oder Deserialize
-Aufruf zu einem anderen zu persistieren, rooten Sie die ReferenceResolver-Instanz auf der Website von Serialize
/Deserialize
. Der folgende Code zeigt ein Beispiel für dieses Szenario:
- Sie verfügen über eine Liste von
Employee
-Objekten, und Sie müssen jedes einzeln serialisieren. - Sie möchten die Verweise nutzen, die im Resolver für
ReferenceHandler
gespeichert sind.
Hier sehen Sie die Employee
-Klasse:
public class Employee
{
public string? Name { get; set; }
public Employee? Manager { get; set; }
public List<Employee>? DirectReports { get; set; }
}
Eine Klasse, die von ReferenceResolver abgeleitet wird, speichert die Verweise in einem Wörterbuch:
class MyReferenceResolver : ReferenceResolver
{
private uint _referenceCount;
private readonly Dictionary<string, object> _referenceIdToObjectMap = [];
private readonly Dictionary<object, string> _objectToReferenceIdMap = new (ReferenceEqualityComparer.Instance);
public override void AddReference(string referenceId, object value)
{
if (!_referenceIdToObjectMap.TryAdd(referenceId, value))
{
throw new JsonException();
}
}
public override string GetReference(object value, out bool alreadyExists)
{
if (_objectToReferenceIdMap.TryGetValue(value, out string? referenceId))
{
alreadyExists = true;
}
else
{
_referenceCount++;
referenceId = _referenceCount.ToString();
_objectToReferenceIdMap.Add(value, referenceId);
alreadyExists = false;
}
return referenceId;
}
public override object ResolveReference(string referenceId)
{
if (!_referenceIdToObjectMap.TryGetValue(referenceId, out object? value))
{
throw new JsonException();
}
return value;
}
}
Eine Klasse, die von ReferenceHandler abgeleitet wird, enthält eine Instanz von MyReferenceResolver
und erstellt nur bei Bedarf eine neue Instanz (in diesem Beispiel in einer Methode namens Reset
):
class MyReferenceHandler : ReferenceHandler
{
public MyReferenceHandler() => Reset();
private ReferenceResolver? _rootedResolver;
public override ReferenceResolver CreateResolver() => _rootedResolver!;
public void Reset() => _rootedResolver = new MyReferenceResolver();
}
Wenn der Beispielcode das Serialisierungsprogramm aufruft, wird eine JsonSerializerOptions-Instanz verwendet, in der die ReferenceHandler-Eigenschaft auf eine Instanz von MyReferenceHandler
festgelegt ist. Wenn Sie diesem Muster folgen, achten Sie unbedingt darauf, das ReferenceResolver
-Wörterbuch zurückzusetzen, wenn Sie die Serialisierung abgeschlossen haben, damit es nicht ständig weiter anwächst.
var options = new JsonSerializerOptions
{
WriteIndented = true
};
var myReferenceHandler = new MyReferenceHandler();
options.ReferenceHandler = myReferenceHandler;
string json;
foreach (Employee emp in employees)
{
json = JsonSerializer.Serialize(emp, options);
DoSomething(json);
}
// Reset after serializing to avoid out of bounds memory growth in the resolver.
myReferenceHandler.Reset();
Ignorieren von Zirkelbezügen
Anstatt Zirkelbezüge zu behandeln, können Sie sie ignorieren. Zum Ignorieren von Zirkelbezügen legen Sie ReferenceHandler auf IgnoreCycles fest. Der Serialisierer legt Eigenschaften von Zirkelbezügen auf null
fest, wie im folgenden Beispiel gezeigt:
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SerializeIgnoreCycles
{
public class Employee
{
public string? Name { get; set; }
public Employee? Manager { get; set; }
public List<Employee>? DirectReports { get; set; }
}
public class Program
{
public static void Main()
{
Employee tyler = new()
{
Name = "Tyler Stein"
};
Employee adrian = new()
{
Name = "Adrian King"
};
tyler.DirectReports = new List<Employee> { adrian };
adrian.Manager = tyler;
JsonSerializerOptions options = new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true
};
string tylerJson = JsonSerializer.Serialize(tyler, options);
Console.WriteLine($"Tyler serialized:\n{tylerJson}");
Employee? tylerDeserialized =
JsonSerializer.Deserialize<Employee>(tylerJson, options);
Console.WriteLine(
"Tyler is manager of Tyler's first direct report: ");
Console.WriteLine(
tylerDeserialized?.DirectReports?[0]?.Manager == tylerDeserialized);
}
}
}
// Produces output like the following example:
//
//Tyler serialized:
//{
// "Name": "Tyler Stein",
// "Manager": null,
// "DirectReports": [
// {
// "Name": "Adrian King",
// "Manager": null,
// "DirectReports": null
// }
// ]
//}
//Tyler is manager of Tyler's first direct report:
//False
Im vorstehenden Beispiel wird Manager
unter Adrian King
als null
serialisiert, um den Zirkelbezug zu vermeiden. Dieses Verhalten bietet folgende Vorteile gegenüber ReferenceHandler.Preserve:
- Es verringert die Größe der Nutzdaten.
- Es generiert JSON, der für andere Serialisierer als System.Text.Json und Newtonsoft.Jsonverständlich ist.
Dieses Verhalten hat folgende Nachteile:
- Stiller Datenverlust.
- Die Daten können keinen Roundtrip von JSON zurück zum Quellobjekt zurücklegen.