Настройка контракта JSON

Библиотека System.Text.Json создает контракт JSON для каждого типа .NET, который определяет способ сериализации и десериализации типа. Контракт является производным от фигуры типа, которая включает такие характеристики, как его свойства и поля, а также то, реализуется IEnumerable ли он или IDictionary интерфейс. Типы сопоставляются с контрактами во время выполнения с помощью отражения или во время компиляции с помощью генератора источника.

Начиная с .NET 7, вы можете настроить эти контракты JSON, чтобы обеспечить более контроль над преобразованием типов в JSON и наоборот. В следующем списке показаны лишь некоторые примеры типов настроек, которые можно сделать для сериализации и десериализации:

  • Сериализация частных полей и свойств.
  • Поддержка нескольких имен для одного свойства (например, если предыдущая версия библиотеки использовала другое имя).
  • Игнорировать свойства с определенным именем, типом или значением.
  • Различает явные null значения и отсутствие значения в полезных данных JSON.
  • Атрибуты поддержки System.Runtime.Serialization , такие как DataContractAttribute. Дополнительные сведения см. в разделе Атрибуты System.Runtime.Serialization.
  • Создает исключение, если JSON содержит свойство, которое не является частью целевого типа. Дополнительные сведения см. в разделе "Обработка отсутствующих элементов".

Как принять участие

Существует два способа подключения к настройке. Оба включают получение сопоставителя, задание которого заключается в предоставлении экземпляра JsonTypeInfo для каждого типа, который необходимо сериализовать.

  • Вызывая DefaultJsonTypeInfoResolver() конструктор для получения JsonSerializerOptions.TypeInfoResolver и добавления пользовательских действий в его Modifiers свойство.

    Например:

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

    Если добавить несколько модификаторов, они будут вызываться последовательно.

  • Написав пользовательский сопоставитель, реализующий IJsonTypeInfoResolver.

    • Если тип не обрабатывается, IJsonTypeInfoResolver.GetTypeInfo возвращается null для этого типа.
    • Вы также можете объединить настраиваемый сопоставитель с другими пользователями, например сопоставитель по умолчанию. Сопоставители будут запрашиваться в порядке, пока JsonTypeInfo не будет возвращено ненулевое значение для типа.

Настраиваемые аспекты

Свойство JsonTypeInfo.Kind указывает, как преобразователь сериализует заданный тип, например как объект или массив, а также сериализуется ли его свойства. Это свойство можно запросить, чтобы определить, какие аспекты контракта JSON типа можно настроить. Существует четыре различных типа:

JsonTypeInfo.Kind Description
JsonTypeInfoKind.Object Преобразователь сериализует тип в объект JSON и использует его свойства. Этот вид используется для большинства типов классов и структур и обеспечивает большую гибкость.
JsonTypeInfoKind.Enumerable Преобразователь сериализует тип в массив JSON. Этот тип используется для типов, таких как List<T> и массив.
JsonTypeInfoKind.Dictionary Преобразователь сериализует тип в объект JSON. Этот тип используется для таких типов, как Dictionary<K, V>.
JsonTypeInfoKind.None Преобразователь не указывает, как будет сериализовать тип или какие JsonTypeInfo свойства он будет использовать. Этот тип используется для таких типов, как System.Object, intи stringдля всех типов, использующих настраиваемый преобразователь.

Модификаторы

Модификатор — это 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
        }
    }
}

Обратите внимание, что значение RoundTrips увеличивается при каждом Product десериализации экземпляра.

Пример. Сериализация частных полей

По умолчанию 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.

Другие способы настройки сериализации

Помимо настройки контракта, существуют и другие способы влияния на поведение сериализации и десериализации, в том числе следующие:

  • Используя атрибуты, производные от JsonAttribute, например, JsonIgnoreAttribute и JsonPropertyOrderAttribute.
  • Изменив JsonSerializerOptions, например, чтобы задать политику именования или сериализовать значения перечисления в виде строк вместо чисел.
  • Написав пользовательский преобразователь, выполняющий фактическую работу при написании JSON и во время десериализации, создание объекта.

Настройка контракта является улучшением по сравнению с этими предварительно существующими настройками, так как у вас может не быть доступа к типу для добавления атрибутов, и написание пользовательского преобразователя является сложным и вредит производительности.

См. также