閱讀英文

共用方式為


如何在 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

此功能無法用來保留實值型別或不可變型別。 還原序列化時,在整個承載已讀取之後,不可變型別的執行個體就會建立。 因此,如果相同執行個體的參考出現在 JSON 承載內,就無法對該執行個體進行還原序列化。

如為實值型別、不可變型別和陣列,不會有任何參考中繼資料會序列化。 在還原序列化時,如果發現 $ref$id,例外狀況就會擲回。 不過,實值型別會忽略 $id (而且 $values 在集合的情況下),以便使用 還原串行化已 Newtonsoft.Json串行化的承載,這會串行化這類類型的元數據。

為了判斷物件是否相等,System.Text.Json 會使用 ReferenceEqualityComparer.Instance,它會在比較兩個物件執行個體時使用相等參考 (Object.ReferenceEquals(Object, Object)),而不是相等值 (Object.Equals(Object))。

如需詳細瞭解如何序列化和還原序列化參考,請參閱ReferenceHandler.Preserve

ReferenceResolver 類別會定義序列化和還原序列化時保留參考的行為。 建立衍生類別以指定自訂行為。 如需範例,請參閱 GuidReferenceResolver (英文)。

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

根據預設,參考資料只會在每次呼叫 SerializeDeserialize 時建立快取。 若要保存一個或呼叫另一Serialize個的參考,請在的呼叫月臺/SerializeDeserialize中根ReferenceResolver目錄實例。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 返回來源物件。

另請參閱