Сохранение ссылок и обработка или пропуск циклических ссылок в System.Text.Json

В этой статье показано, как сохранять ссылки и обрабатывать или игнорировать циклические ссылки при использовании System.Text.Json для сериализации и десериализации JSON в .NET

Сохранение ссылок и обработка циклических ссылок

Для сохранения ссылок и обработки циклических ссылок задайте для свойства ReferenceHandler значение Preserve. Этот параметр приводит к следующему поведению:

  • При сериализации:

    При написании сложных типов сериализатор также записывает свойства метаданных ($id, $values, а также $ref).

  • При десериализации:

    Ожидаются метаданные (не обязательно), и десериализатор пытается их распознать.

В следующем коде показано использование параметра 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

Эту функцию нельзя использовать для сохранения типов значений или неизменяемых типов. При десериализации экземпляр неизменяемого типа создается после считывания всех полезных данных. Поэтому для того же экземпляра невозможно будет выполнить десериализацию, если в полезных данных JSON появляется ссылка на него.

Для типов значений, неизменяемых типов и массивов ссылочные метаданные не сериализуются. Если при десериализации было найдено $ref или $id, создается исключение. Однако типы значений игнорируют $id$values для коллекций), чтобы позволить десериализацию полезных данных, сериализованных с помощью Newtonsoft.Json. Newtonsoft.Json выполняет сериализацию метаданных для таких типов.

Чтобы определить, равны ли объекты, используется ReferenceEqualityComparer.Instanceметод System.Text.Json равенства ссылок (Object.ReferenceEquals(Object, Object)) вместо равенства значений (Object.Equals(Object)) при сравнении двух экземпляров объектов.

Больше о сериализации и десериализации ссылок см. в статье ReferenceHandler.Preserve.

Класс ReferenceResolver определяет поведение сохранения ссылок при сериализации и десериализации. Создайте производный класс, чтобы указать настраиваемое поведение. Пример см. в статье GuidReferenceResolver.

Сохранение ссылочных метаданных в нескольких вызовах сериализации и десериализации

По умолчанию ссылочные данные кэшируются только для каждого вызова Serialize или Deserialize. Чтобы сохранить ссылки из одного Serialize/Deserialize вызова на другой, корень ReferenceResolver экземпляра на сайте Serialize/Deserializeвызова. В следующем коде показан пример для этого сценария:

  • У вас есть список Employee объектов и необходимо сериализовать каждый из них по отдельности.
  • Вы хотите воспользоваться ссылками, сохраненными в сопоставителье.ReferenceHandler

Так выглядит класс Employee :

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

Класс, производный от ReferenceResolver хранения ссылок в словаре:

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

Класс, производный от ReferenceHandler экземпляра MyReferenceResolver и создающий новый экземпляр только при необходимости (в методе, именованном Reset в этом примере):

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

Когда пример кода вызывает сериализатор, он использует JsonSerializerOptions экземпляр, в котором ReferenceHandler свойство задано экземпляром MyReferenceHandler. Когда вы следуйте этому шаблону, обязательно сбросьте ReferenceResolver словарь после завершения сериализации, чтобы сохранить его от постоянного роста.

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

Игнорировать циклические ссылки

Вместо обработки циклических ссылок их можно игнорировать. Чтобы игнорировать циклические ссылки, задайте для ReferenceHandlerIgnoreCyclesпараметра . Сериализатор задает свойства nullциклической ссылки, как показано в следующем примере:

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

В предыдущем примере Manager сериализуется Adrian King как null избежать циклической ссылки. Это поведение имеет следующие преимущества:ReferenceHandler.Preserve

  • Он уменьшает размер полезных данных.
  • Он создает JSON, понятный для сериализаторов, отличных System.Text.Json от и Newtonsoft.Json.

Это поведение имеет следующие недостатки:

  • Автоматическая потеря данных.
  • Данные не могут выполнить круговую поездку из JSON обратно в исходный объект.

См. также