如何在 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串行化的承載,這會串行化這類類型的元數據。
為了判斷物件是否相等,System.Text.Json 會使用 ReferenceEqualityComparer.Instance,它會在比較兩個物件執行個體時使用相等參考 (Object.ReferenceEquals(Object, Object)),而不是相等值 (Object.Equals(Object))。
如需詳細瞭解如何序列化和還原序列化參考,請參閱ReferenceHandler.Preserve。
ReferenceResolver 類別會定義序列化和還原序列化時保留參考的行為。 建立衍生類別以指定自訂行為。 如需範例,請參閱 GuidReferenceResolver (英文)。
保存多個序列化和還原序列化呼叫的參考中繼資料
根據預設,參考資料只會在每次呼叫 Serialize 或 Deserialize 時建立快取。 若要保存一個或呼叫另一Serialize
個的參考,請在的呼叫月臺/Serialize
Deserialize
中根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 返回來源物件。