如何在 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 = [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 (英文)。

保存多個序列化和還原序列化呼叫的參考中繼資料

根據預設,參考資料只會在每次呼叫 SerializeDeserialize 時建立快取。 若要將一個 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();

忽略循環參考

您可以不處理循環參考,直接忽略即可。 若要忽略循環參考,請將 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 返回來源物件。

另請參閱