System.Text.Json で参照を保持し、循環参照を処理または無視する方法
この記事では、System.Text.Json を使用して .NET で 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 によって、2 つのオブジェクト インスタンスを比較するときに、値の等価性 (Object.Equals(Object)) ではなく、参照の等価性 (Object.ReferenceEquals(Object, 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 = [];
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();
}
このサンプル コードは、シリアライザーを呼び出すときに、ReferenceHandler プロパティが MyReferenceHandler
のインスタンスに設定されている JsonSerializerOptions インスタンスを使用します。 このパターンに従う場合は、シリアル化が終了したときに必ず 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 より優れています。
- ペイロードのサイズが減少します。
- System.Text.Json と Newtonsoft.Json 以外のシリアライザーで理解できる JSON が作成されます。
この動作には、次のような欠点があります。
- データの損失に気付きません。
- データは、JSON からソース オブジェクトにラウンドトリップできません。
関連項目
.NET