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. Werttypen ignorieren jedoch $id (und $values im Falle von Sammlungen), um die Deserialisierung von Nutzdaten zu ermöglichen, die mit Newtonsoft.Json serialisiert wurden. Newtonsoft.Json serialisiert die Metadaten für solche Typen.

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. Zum Beibehalten von Verweisen von einem Serialize/Deserialize-Aufruf zum nächsten rooten Sie die ReferenceResolver-Instanz auf der Aufrufwebsite 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 MyReferenceHandlerfestgelegt 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.

Weitere Informationen