System.Text.Json을 사용하여 참조를 유지하고 순환 참조를 처리 또는 무시하는 방법

이 문서에서는 System.Text.Json을 사용하여 .NET에서 JSON을 직렬화 및 역직렬화하는 동안 참조를 유지하고 순환 참조를 처리하거나 무시하는 방법을 보여 줍니다.

참조를 보존하고 순환 참조를 처리

참조를 보존하고 순환 참조를 처리하려면 ReferenceHandlerPreserve로 설정합니다. 이렇게 설정하면 다음과 같은 동작이 발생합니다.

  • 직렬화 시:

    복합 형식을 쓸 때 직렬 변환기는 메타데이터 속성($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됩니다. 하지만 Newtonsoft.Json을 사용하여 직렬화된 페이로드를 역직렬화할 수 있도록 값 형식은 $id(및 컬렉션의 경우에는 $values)를 무시합니다. Newtonsoft.Json은 이러한 형식의 메타데이터를 직렬화합니다.

개체가 같은지 확인하기 위해 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();

순환 참조 무시

순환 참조를 처리하는 대신 무시할 수 있습니다. 순환 참조를 무시하려면 ReferenceHandlerIgnoreCycles로 설정합니다. 직렬 변환기는 다음 예제와 같이 순환 참조 속성을 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에서 원본 개체로 왕복할 수 없습니다.

참고 항목