Jak zachować odwołania i obsługiwać lub ignorować odwołania cykliczne w programie System.Text.Json

W tym artykule pokazano, jak zachować odwołania i obsługiwać lub ignorować odwołania cykliczne podczas używania System.Text.Json do serializacji i deserializacji kodu JSON na platformie .NET

Zachowywanie odwołań i obsługa odwołań cyklicznych

Aby zachować odwołania i obsłużyć odwołania cykliczne, ustaw wartość ReferenceHandlerPreserve. To ustawienie powoduje następujące zachowanie:

  • W przypadku serializacji:

    Podczas pisania typów złożonych serializator zapisuje również właściwości metadanych ($id, $values, i $ref).

  • Podczas deserializacji:

    Metadane są oczekiwane (choć nie obowiązkowe), a deserializator próbuje je zrozumieć.

Poniższy kod ilustruje użycie Preserve ustawienia.

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

Tej funkcji nie można używać do zachowywania typów wartości ani typów niezmiennych. W przypadku deserializacji wystąpienie niezmiennego typu jest tworzone po odczytaniu całego ładunku. Dlatego deserializowanie tego samego wystąpienia byłoby niemożliwe, jeśli odwołanie do niego pojawia się w ładunku JSON.

W przypadku typów wartości, niezmiennych typów i tablic żadne metadane referencyjne nie są serializowane. W przypadku deserializacji zgłaszany jest wyjątek, jeśli $ref zostanie znaleziony lub $id znaleziony. Jednak typy wartości ignorują $id (i $values w przypadku kolekcji), aby umożliwić deserializowanie ładunków, które zostały serializowane przy użyciu metody Newtonsoft.Json. Newtonsoft.Json program serializuje metadane dla takich typów.

Aby określić, czy obiekty są równe, System.Text.Json używa metody ReferenceEqualityComparer.Instance, która używa równości odwołań () zamiast równości wartości (Object.ReferenceEquals(Object, Object)Object.Equals(Object)) podczas porównywania dwóch wystąpień obiektów.

Aby uzyskać więcej informacji na temat serializacji i deserializacji odwołań, zobacz ReferenceHandler.Preserve.

Klasa ReferenceResolver definiuje zachowanie zachowania odwołań do serializacji i deserializacji. Utwórz klasę pochodną, aby określić zachowanie niestandardowe. Aby zapoznać się z przykładem, zobacz GuidReferenceResolver.

Utrwalanie metadanych odwołania między wieloma wywołaniami serializacji i deserializacji

Domyślnie dane referencyjne są buforowane tylko dla każdego wywołania metody Serialize lub Deserialize. Aby utrwalać odwołania z jednego Serialize/Deserialize wywołania do innego, root ReferenceResolver wystąpienia w lokacji Serialize/Deserializewywołania klasy . Poniższy kod przedstawia przykład dla tego scenariusza:

  • Masz listę Employee obiektów i musisz serializować każdy z nich indywidualnie.
  • Chcesz skorzystać z odwołań zapisanych w narzędziu rozpoznawania nazw dla elementu ReferenceHandler.

Employee Oto klasa:

public class Employee
{
    public string? Name { get; set; }
    public Employee? Manager { get; set; }
    public List<Employee>? DirectReports { get; set; }
}

Klasa, która pochodzi z ReferenceResolver magazynów odwołań w słowniku:

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

Klasa, która pochodzi z ReferenceHandlerMyReferenceResolver wystąpienia klasy i tworzy nowe wystąpienie tylko wtedy, gdy jest to konieczne (w metodzie o nazwie Reset w tym przykładzie):

class MyReferenceHandler : ReferenceHandler
{
    public MyReferenceHandler() => Reset();
    private ReferenceResolver? _rootedResolver;
    public override ReferenceResolver CreateResolver() => _rootedResolver!;
    public void Reset() => _rootedResolver = new MyReferenceResolver();
}

Gdy przykładowy kod wywołuje serializator, używa JsonSerializerOptions wystąpienia, w którym ReferenceHandler właściwość jest ustawiona na wystąpienie MyReferenceHandlerklasy . Po przestrzeganiu tego wzorca pamiętaj, aby zresetować słownik po zakończeniu ReferenceResolver serializacji, aby zachować jego rozwój na zawsze.

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();

Ignoruj odwołania cykliczne

Zamiast obsługiwać odwołania cykliczne, można je zignorować. Aby zignorować odwołania cykliczne, ustaw wartość ReferenceHandlerIgnoreCycles. Serializator ustawia właściwości odwołania cyklicznego na null, jak pokazano w poniższym przykładzie:

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

W poprzednim przykładzie Manager element under Adrian King jest serializowany, aby null uniknąć odwołania cyklicznego. To zachowanie ma następujące zalety w porównaniu z ReferenceHandler.Preserve:

  • Zmniejsza rozmiar ładunku.
  • Tworzy kod JSON zrozumiały dla serializatorów innych niż System.Text.Json i Newtonsoft.Json.

To zachowanie ma następujące wady:

  • Dyskretna utrata danych.
  • Dane nie mogą wykonać rundy z pliku JSON z powrotem do obiektu źródłowego.

Zobacz też