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
가 발견되면 예외가 throw됩니다. 그러나 값 형식은 무시 $id
(컬렉션 $values
의 경우)하여 이러한 형식에 대한 메타데이터를 serialize하는 를 사용하여 Newtonsoft.Jsonserialize된 페이로드를 역직렬화할 수 있도록 합니다.
개체가 같은지 확인하기 위해 System.Text.Json은 두 개체 인스턴스를 비교할 때 값 같음(Object.Equals(Object)) 대신 참조 같음(Object.ReferenceEquals(Object, Object))을 사용하는 ReferenceEqualityComparer.Instance을 사용합니다.
참조가 직렬화 및 역직렬화되는 방법에 대한 자세한 내용은 ReferenceHandler.Preserve을 참조하세요.
ReferenceResolver 클래스는 직렬화 및 역직렬화 시 참조를 보존하는 동작을 정의합니다. 파생 클래스를 만들어 사용자 지정 동작을 지정합니다. 예제는 GuidReferenceResolver를 참조하세요.
여러 serialization 및 deserialization 호출에서 참조 메타데이터 유지
기본적으로 참조 데이터는 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
로 serialize됩니다. 이 동작은 ReferenceHandler.Preserve에 비해 다음과 같은 이점이 있습니다.
- 페이로드 크기가 줄어듭니다.
- System.Text.Json 및 Newtonsoft.Json 이외의 직렬 변환기에 대해 적절한 JSON이 생성됩니다.
이 동작에는 다음과 같은 단점이 있습니다.
- 데이터가 자동으로 손실됩니다.
- 데이터는 JSON에서 원본 개체로 왕복할 수 없습니다.
참고 항목
.NET