如何在 System.Text.Json 中保留引用并处理或忽略循环引用

本文介绍在 .NET 中使用 System.Text.Json 序列化和反序列化 JSON 时,如何保留引用并处理或忽略循环引用

保留引用并处理循环引用

若要保留引用并处理循环引用,请将 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 = new List<Employee> { 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 为此类类型序列化元数据。

为了确定对象是否相等,System.Text.Json 使用 ReferenceEqualityComparer.Instance,后者在比较两个对象实例时使用引用相等性 (Object.ReferenceEquals(Object, Object)) 而不是值相等性 (Object.Equals(Object))。

有关如何序列化和反序列化引用的详细信息,请参阅 ReferenceHandler.Preserve

ReferenceResolver 类定义在序列化和反序列化过程中保留引用的行为。 创建派生类以指定自定义行为。 有关示例,请参阅 GuidReferenceResolver

跨多个序列化和反序列化调用保留引用元数据

默认情况下,每次调用 或 时,仅缓存引用 Serialize 数据 Deserialize。 若要保留从一个 Serialize/Deserialize 调用到另一个调用的引用,请在 Serialize/Deserialize 的调用站点中将 ReferenceResolver 实例放在根目录。 下面的代码演示了此方案的一个示例:

  • 你有一个 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 = new ();
    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();
options.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();

.NET Core 3.1 中的 System.Text.Json 仅支持按值进行进行序列化,并对循环引用引发异常。

忽略循环引用

可以忽略循环引用,而不是处理循环引用。 若要忽略循环引用,请将 ReferenceHandler 设置为 IgnoreCycles。 序列化程序将循环引用属性设置为 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

在上一示例中,Adrian King 下的 Manager 序列化为 null 以避开循环引用。 此行为相较于 ReferenceHandler.Preserve 具有以下优势:

  • 可减小有效负载大小。
  • 可创建序列化程序易于理解的 JSON(System.Text.Json 和 Newtonsoft.Json 除外)。

此行为相具有以下缺点:

  • 数据丢失时无提示。
  • 数据无法从 JSON 往返回源对象。

.NET 5 及更早版本的 System.Text.Json 不支持 ReferenceHandler.IgnoreCycles

请参阅