다음을 통해 공유


JSON 계약 사용자 지정

System.Text.Json 라이브러리는 해당 형식을 직렬화하고 역직렬화하는 방법을 정의하는 각 .NET 형식에 대해 JSON 계약을 생성합니다. 계약은 해당 형식의 셰이프에서 파생됩니다. 여기에는 속성 및 필드와 같은 특성과 IEnumerable 또는 IDictionary 인터페이스를 구현하는지 여부가 포함됩니다. 형식은 리플렉션을 사용하여 런타임에 계약에 매핑되거나 또는 소스 생성기를 사용하여 컴파일 시간에 계약에 매핑됩니다.

.NET 7부터 이러한 JSON 계약을 사용자 지정하여 형식이 JSON으로 변환되는 방식을 보다 자세히 제어할 수 있으며 그 반대의 경우도 마찬가지입니다. 다음 목록에서는 직렬화 및 역직렬화에 수행할 수 있는 사용자 지정 유형의 몇 가지 예만 보여줍니다.

  • 프라이빗 필드 및 속성을 직렬화합니다.
  • 단일 속성에 대해 여러 이름을 지원합니다(예: 이전 라이브러리 버전에서 다른 이름을 사용한 경우).
  • 특정 이름, 형식 또는 값이 있는 속성은 무시합니다.
  • 명시적 null 값과 JSON 페이로드의 값 부족을 구분합니다.
  • DataContractAttribute 같은 System.Runtime.Serialization 특성을 지원합니다. 자세한 내용은 System.Runtime.Serialization 특성을 참조하세요.
  • JSON에 대상 형식의 일부가 아닌 속성이 포함된 경우 예외를 throw합니다. 자세한 내용은 누락된 멤버 처리를 참조하세요.

옵트인하는 방법

사용자 지정에 연결하는 방법에는 두 가지가 있습니다. 두 가지 방법은 모두 직렬화해야 하는 각 형식에 대한 JsonTypeInfo 인스턴스를 제공하는 작업을 수행하는 확인자 가져오기를 포함합니다.

  • DefaultJsonTypeInfoResolver() 생성자를 호출하여 JsonSerializerOptions.TypeInfoResolver를 가져오고 그 Modifiers 속성에 사용자 지정 작업을 추가합니다.

    다음은 그 예입니다.

    JsonSerializerOptions options = new()
    {
        TypeInfoResolver = new DefaultJsonTypeInfoResolver
        {
            Modifiers =
            {
                MyCustomModifier1,
                MyCustomModifier2
            }
        }
    };
    

    여러 한정자를 추가하면 순차적으로 호출됩니다.

  • IJsonTypeInfoResolver를 구현하는 사용자 지정 확인자를 작성합니다.

    • 하나의 형식이 처리되지 않으면 IJsonTypeInfoResolver.GetTypeInfo는 해당 형식에 대해 null을 반환해야 합니다.
    • 사용자 지정 확인자(예: 기본 확인자)를 다른 사용자의 그것과 결합할 수도 있습니다. 확인자는 null이 아닌 JsonTypeInfo 값이 해당 형식에 대해 반환될 때까지 순서대로 쿼리됩니다.

구성 가능한 측면

JsonTypeInfo.Kind 속성은 변환기가 지정된 형식을 직렬화하는 방법(예: 개체 또는 배열로서 직렬화)과 그 속성이 직렬화되는지 여부를 나타냅니다. 이 속성을 쿼리하여 구성할 수 있는 형식의 JSON 계약의 측면을 확인할 수 있습니다. 이 속성은 다음의 네 가지 종류가 있습니다.

JsonTypeInfo.Kind 설명
JsonTypeInfoKind.Object 변환기는 형식을 JSON 개체로 직렬화하고 그 속성을 사용합니다. 이 종류는 대부분의 클래스 및 구조체 형식에 사용되며 가장 유연하게 사용할 수 있습니다.
JsonTypeInfoKind.Enumerable 변환기는 형식을 JSON 배열로 직렬화합니다. 이 종류는 List<T> 및 배열과 같은 형식에 사용됩니다.
JsonTypeInfoKind.Dictionary 변환기는 해당 형식을 JSON 개체로 직렬화합니다. 이 종류는 Dictionary<K, V>과 같은 형식에 사용됩니다.
JsonTypeInfoKind.None 변환기는 해당 형식을 직렬화하는 방법 또는 그 형식이 사용할 JsonTypeInfo 속성을 지정하지 않습니다. 이 종류는 System.Object, intstring과 같은 형식과 사용자 지정 변환기를 사용하는 모든 형식에 사용됩니다.

한정자

한정자는 Action<JsonTypeInfo>이거나 또는 하나의 인수로 계약의 현재 상태를 가져오고 계약을 수정하는 JsonTypeInfo 매개 변수가 있는 메서드입니다. 예를 들어 지정한 JsonTypeInfo에서 미리 채워진 속성을 반복하여 관심 있는 속성을 찾은 다음, 해당 JsonPropertyInfo.Get 속성(직렬화용) 또는 JsonPropertyInfo.Set 속성(역직렬화용)을 수정할 수 있습니다. 또는 JsonTypeInfo.CreateJsonPropertyInfo(Type, String)을 사용하여 새 속성을 생성하고 JsonTypeInfo.Properties 컬렉션에 추가할 수 있습니다.

다음 표에서는 수행할 수 있는 수정 사항과 이를 수행하는 방법을 보여줍니다.

수정 적용 가능한 JsonTypeInfo.Kind 수행 방법
속성 값 사용자 지정 JsonTypeInfoKind.Object 해당 속성에 대한 JsonPropertyInfo.Get 대리자(직렬화용) 또는 JsonPropertyInfo.Set 대리자(역직렬화용)를 수정합니다. 속성 값 증가
속성 추가/제거 JsonTypeInfoKind.Object JsonTypeInfo.Properties 목록에서 항목을 추가하거나 제거합니다. 프라이빗 필드 직렬화
조건부로 속성 직렬화 JsonTypeInfoKind.Object 해당 속성에 대한 JsonPropertyInfo.ShouldSerialize 조건자를 수정합니다. 특정 형식의 속성 무시
특정 형식에 대한 숫자 처리 사용자 지정 JsonTypeInfoKind.None 해당 형식의 JsonTypeInfo.NumberHandling 값을 수정합니다. int 값을 문자열로 허용

예: 속성 값 증가

한정자가 JsonPropertyInfo.Set 대리자를 수정하여 역직렬화 시 특정 속성의 값을 증가시키는 다음 예제를 생각해 보세요. 이 예제에서는 한정자를 정의하는 것 외에도 값을 증분해야 하는 속성을 찾는 데 사용하는 새 특성도 소개합니다. 속성을 사용자 지정하는 예제입니다.

using System.Text.Json;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    // Custom attribute to annotate the property
    // we want to be incremented.
    [AttributeUsage(AttributeTargets.Property)]
    class SerializationCountAttribute : Attribute
    {
    }

    // Example type to serialize and deserialize.
    class Product
    {
        public string Name { get; set; } = "";
        [SerializationCount]
        public int RoundTrips { get; set; }
    }

    public class SerializationCountExample
    {
        // Custom modifier that increments the value
        // of a specific property on deserialization.
        static void IncrementCounterModifier(JsonTypeInfo typeInfo)
        {
            foreach (JsonPropertyInfo propertyInfo in typeInfo.Properties)
            {
                if (propertyInfo.PropertyType != typeof(int))
                    continue;

                object[] serializationCountAttributes = propertyInfo.AttributeProvider?.GetCustomAttributes(typeof(SerializationCountAttribute), true) ?? Array.Empty<object>();
                SerializationCountAttribute? attribute = serializationCountAttributes.Length == 1 ? (SerializationCountAttribute)serializationCountAttributes[0] : null;

                if (attribute != null)
                {
                    Action<object, object?>? setProperty = propertyInfo.Set;
                    if (setProperty is not null)
                    {
                        propertyInfo.Set = (obj, value) =>
                        {
                            if (value != null)
                            {
                                // Increment the value by 1.
                                value = (int)value + 1;
                            }

                            setProperty (obj, value);
                        };
                    }
                }
            }
        }

        public static void RunIt()
        {
            var product = new Product
            {
                Name = "Aquafresh"
            };

            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { IncrementCounterModifier }
                }
            };

            // First serialization and deserialization.
            string serialized = JsonSerializer.Serialize(product, options);
            Console.WriteLine(serialized);
            // {"Name":"Aquafresh","RoundTrips":0}

            Product deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
            Console.WriteLine($"{deserialized.RoundTrips}");
            // 1

            // Second serialization and deserialization.
            serialized = JsonSerializer.Serialize(deserialized, options);
            Console.WriteLine(serialized);
            // { "Name":"Aquafresh","RoundTrips":1}

            deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
            Console.WriteLine($"{deserialized.RoundTrips}");
            // 2
        }
    }
}

출력에서 Product 인스턴스가 역직렬화될 때마다 RoundTrips의 값이 증가합니다.

예제: 프라이빗 필드 직렬화

기본적으로 System.Text.Json은 프라이빗 필드와 속성을 무시합니다. 이 예제에서는 해당 기본값을 변경하기 위해 클래스 차원의 새 특성 JsonIncludePrivateFieldsAttribute를 추가합니다. 한정자가 하나의 형식에서 특성을 찾으면 해당 형식의 모든 프라이빗 필드를 JsonTypeInfo에 새 속성으로 추가합니다.

using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    public class JsonIncludePrivateFieldsAttribute : Attribute { }

    [JsonIncludePrivateFields]
    public class Human
    {
        private string _name;
        private int _age;

        public Human()
        {
            // This constructor should be used only by deserializers.
            _name = null!;
            _age = 0;
        }

        public static Human Create(string name, int age)
        {
            Human h = new()
            {
                _name = name,
                _age = age
            };

            return h;
        }

        [JsonIgnore]
        public string Name
        {
            get => _name;
            set => throw new NotSupportedException();
        }

        [JsonIgnore]
        public int Age
        {
            get => _age;
            set => throw new NotSupportedException();
        }
    }

    public class PrivateFieldsExample
    {
        static void AddPrivateFieldsModifier(JsonTypeInfo jsonTypeInfo)
        {
            if (jsonTypeInfo.Kind != JsonTypeInfoKind.Object)
                return;

            if (!jsonTypeInfo.Type.IsDefined(typeof(JsonIncludePrivateFieldsAttribute), inherit: false))
                return;

            foreach (FieldInfo field in jsonTypeInfo.Type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
            {
                JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(field.FieldType, field.Name);
                jsonPropertyInfo.Get = field.GetValue;
                jsonPropertyInfo.Set = field.SetValue;

                jsonTypeInfo.Properties.Add(jsonPropertyInfo);
            }
        }

        public static void RunIt()
        {
            var options = new JsonSerializerOptions
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { AddPrivateFieldsModifier }
                }
            };

            var human = Human.Create("Julius", 37);
            string json = JsonSerializer.Serialize(human, options);
            Console.WriteLine(json);
            // {"_name":"Julius","_age":37}

            Human deserializedHuman = JsonSerializer.Deserialize<Human>(json, options)!;
            Console.WriteLine($"[Name={deserializedHuman.Name}; Age={deserializedHuman.Age}]");
            // [Name=Julius; Age=37]
        }
    }
}

프라이빗 필드 이름이 밑줄로 시작하는 경우 필드를 새 JSON 속성으로 추가할 때 이름에서 밑줄을 제거하는 것이 좋습니다.

예제: 특정 형식의 속성 무시

사용자에게 노출하지 않으려는 특정 이름 또는 형식의 속성이 모델에 있는 것일 수 있습니다. 예를 들어 자격 증명을 저장하는 속성이나 페이로드에 사용할 수 없는 일부 정보가 있을 수 있습니다.

다음 예제에서는 특정 형식인 SecretHolder의 속성을 필터링하는 방법을 보여줍니다. 이 작업에서는 IList<T> 확장 메서드를 사용하여 JsonTypeInfo.Properties 목록에서 지정된 형식이 있는 속성을 모두 제거합니다. 필터링된 속성은 계약에서 완전히 사라집니다. 즉, System.Text.Json은 직렬화 또는 역직렬화 중에는 이러한 속성을 보지 않습니다.

using System.Text.Json;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    class ExampleClass
    {
        public string Name { get; set; } = "";
        public SecretHolder? Secret { get; set; }
    }

    class SecretHolder
    {
        public string Value { get; set; } = "";
    }

    class IgnorePropertiesWithType
    {
        private readonly Type[] _ignoredTypes;

        public IgnorePropertiesWithType(params Type[] ignoredTypes)
            => _ignoredTypes = ignoredTypes;

        public void ModifyTypeInfo(JsonTypeInfo ti)
        {
            if (ti.Kind != JsonTypeInfoKind.Object)
                return;

            ti.Properties.RemoveAll(prop => _ignoredTypes.Contains(prop.PropertyType));
        }
    }

    public class IgnoreTypeExample
    {
        public static void RunIt()
        {
            var modifier = new IgnorePropertiesWithType(typeof(SecretHolder));

            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { modifier.ModifyTypeInfo }
                }
            };

            ExampleClass obj = new()
            {
                Name = "Password",
                Secret = new SecretHolder { Value = "MySecret" }
            };

            string output = JsonSerializer.Serialize(obj, options);
            Console.WriteLine(output);
            // {"Name":"Password"}
        }
    }

    public static class ListHelpers
    {
        // IList<T> implementation of List<T>.RemoveAll method.
        public static void RemoveAll<T>(this IList<T> list, Predicate<T> predicate)
        {
            for (int i = 0; i < list.Count; i++)
            {
                if (predicate(list[i]))
                {
                    list.RemoveAt(i--);
                }
            }
        }
    }
}

예제: int 값을 문자열로 허용

아마도 입력 JSON은 숫자 형식 중 하나의 주위에 따옴표를 포함할 수 있지만 다른 형식에는 포함하지 않을 수 있습니다. 클래스를 제어할 수 있는 경우 이 문제를 해결하기 위해 해당 형식에 JsonNumberHandlingAttribute를 배치할 수 있지만 실제로는 배치하지 않습니다. .NET 7 이전에는 이 동작을 수정하기 위해 사용자 지정 변환기를 작성해야 하며, 이를 위해서는 상당한 코드를 작성해야 합니다. 계약 사용자 지정을 사용하여 모든 형식에 대한 숫자 처리 동작을 사용자 지정할 수 있습니다.

다음 예제에서는 모든 int 값에 대한 동작을 변경합니다. 이 예제는 모든 형식에 적용하거나 모든 형식의 특정 속성에 적용하도록 쉽게 조정할 수 있습니다.

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    public class Point
    {
        public int X { get; set; }
        public int Y { get; set; }
    }

    public class AllowIntsAsStringsExample
    {
        static void SetNumberHandlingModifier(JsonTypeInfo jsonTypeInfo)
        {
            if (jsonTypeInfo.Type == typeof(int))
            {
                jsonTypeInfo.NumberHandling = JsonNumberHandling.AllowReadingFromString;
            }
        }

        public static void RunIt()
        {
            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { SetNumberHandlingModifier }
                }
            };

            // Triple-quote syntax is a C# 11 feature.
            Point point = JsonSerializer.Deserialize<Point>("""{"X":"12","Y":"3"}""", options)!;
            Console.WriteLine($"({point.X},{point.Y})");
            // (12,3)
        }
    }
}

문자열에서 int 값을 읽을 수 있도록 허용하는 한정자가 없다면 프로그램은 다음과 같이 예외로 끝났을 것입니다.

처리되지 않은 예외가 발생했습니다. System.Text.Json.JsonException: JSON 값을 System.Int32로 변환할 수 없습니다. 경로: $.X | LineNumber: 0 | BytePositionInLine: 9.

serialization을 사용자 지정하는 다른 방법

계약을 사용자 지정하는 것 외에도 다음을 포함하여 직렬화 및 역직렬화 동작에 영향을 미치는 다른 방법이 있습니다.

  • JsonAttribute에서 파생된 특성(예: JsonIgnoreAttributeJsonPropertyOrderAttribute)을 사용합니다.
  • 예를 들어 JsonSerializerOptions를 수정하여 명명 정책을 설정하거나 열거형 값을 숫자 대신 문자열로 직렬화합니다.
  • JSON을 작성하는 실제 작업을 수행하는 사용자 지정 변환기를 작성하고 역직렬화 중에 개체를 생성합니다.

계약 사용자 지정은 이러한 기존 사용자 지정에 비해 개선되었는데, 그 이유는 특성을 추가할 형식에 액세스할 수 없으며 사용자 지정 변환기를 작성하는 것은 복잡하고 오히려 성능이 저하되기 때문입니다.

참고 항목