Dostosowywanie kontraktu JSON

Biblioteka System.Text.Json tworzy kontrakt JSON dla każdego typu platformy .NET, który definiuje sposób serializacji i deserializacji typu. Kontrakt pochodzi z kształtu typu, który zawiera cechy, takie jak jego właściwości i pola oraz czy implementuje IEnumerable interfejs lub IDictionary . Typy są mapowane na kontrakty w czasie wykonywania przy użyciu odbicia lub w czasie kompilacji przy użyciu generatora źródła.

Począwszy od platformy .NET 7, możesz dostosować te kontrakty JSON, aby zapewnić większą kontrolę nad sposobem konwertowania typów na format JSON i odwrotnie. Na poniższej liście przedstawiono tylko kilka przykładów typów dostosowań, które można wprowadzić w celu serializacji i deserializacji:

  • Serializowanie prywatnych pól i właściwości.
  • Obsługa wielu nazw pojedynczej właściwości (na przykład jeśli poprzednia wersja biblioteki użyła innej nazwy).
  • Ignoruj właściwości o określonej nazwie, typie lub wartości.
  • Rozróżnianie jawnych null wartości i brak wartości w ładunku JSON.
  • Atrybuty obsługi System.Runtime.Serialization , takie jak DataContractAttribute. Aby uzyskać więcej informacji, zobacz Atrybuty System.Runtime.Serialization.
  • Zgłaszanie wyjątku, jeśli kod JSON zawiera właściwość, która nie jest częścią typu docelowego. Aby uzyskać więcej informacji, zobacz Obsługa brakujących elementów członkowskich.

Jak wyrazić zgodę

Istnieją dwa sposoby podłączania do dostosowywania. Oba obejmują uzyskanie narzędzia rozpoznawania nazw, którego zadaniem jest udostępnienie JsonTypeInfo wystąpienia dla każdego typu, który musi być serializowany.

  • Wywołując konstruktor w DefaultJsonTypeInfoResolver() celu uzyskania i dodania JsonSerializerOptions.TypeInfoResolverakcji niestandardowych do jej Modifiers właściwości.

    Na przykład:

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

    Jeśli dodasz wiele modyfikatorów, będą one wywoływane sekwencyjnie.

  • Pisząc niestandardowy program rozpoznawania nazw, który implementuje IJsonTypeInfoResolverelement .

    • Jeśli typ nie jest obsługiwany, IJsonTypeInfoResolver.GetTypeInfo powinien zostać zwrócony null dla tego typu.
    • Możesz również połączyć niestandardowy program rozpoznawania nazw z innymi, na przykład domyślnym modułem rozpoznawania. Narzędzia rozpoznawania nazw będą odpytywane w kolejności do momentu zwrócenia wartości innej niż null JsonTypeInfo dla typu.

Konfigurowalne aspekty

Właściwość JsonTypeInfo.Kind wskazuje, jak konwerter serializuje dany typ — na przykład jako obiekt lub jako tablicę oraz czy jego właściwości są serializowane. Możesz wykonać zapytanie dotyczące tej właściwości, aby określić, które aspekty kontraktu JSON typu można skonfigurować. Istnieją cztery różne rodzaje:

JsonTypeInfo.Kind opis
JsonTypeInfoKind.Object Konwerter serializuje typ do obiektu JSON i używa jego właściwości. Ten rodzaj jest używany dla większości typów klas i struktur i zapewnia największą elastyczność.
JsonTypeInfoKind.Enumerable Konwerter serializuje typ do tablicy JSON. Ten rodzaj jest używany dla typów, takich jak List<T> i tablica.
JsonTypeInfoKind.Dictionary Konwerter serializuje typ do obiektu JSON. Ten rodzaj jest używany dla typów, takich jak Dictionary<K, V>.
JsonTypeInfoKind.None Konwerter nie określa, jak będzie serializować typ lub jakie JsonTypeInfo właściwości będą używane. Ten rodzaj jest używany dla typów, takich jak System.Object, inti , i stringdla wszystkich typów, które używają konwertera niestandardowego.

Modyfikatory

Modyfikator to Action<JsonTypeInfo> metoda lub z parametrem JsonTypeInfo , który pobiera bieżący stan kontraktu jako argument i wprowadza modyfikacje kontraktu. Można na przykład wykonać iterację po wstępnie wypełnionych właściwościach określonych JsonTypeInfo , aby znaleźć odpowiednią właściwość, a następnie zmodyfikować jej JsonPropertyInfo.Get właściwość (na potrzeby serializacji) lub JsonPropertyInfo.Set właściwość (w celu deserializacji). Możesz też utworzyć nową właściwość przy użyciu metody JsonTypeInfo.CreateJsonPropertyInfo(Type, String) i dodać ją do kolekcji JsonTypeInfo.Properties .

W poniższej tabeli przedstawiono modyfikacje, które można wprowadzić i jak je osiągnąć.

Modyfikacji Mające zastosowanie JsonTypeInfo.Kind Jak to osiągnąć Przykład
Dostosowywanie wartości właściwości JsonTypeInfoKind.Object Zmodyfikuj JsonPropertyInfo.Get delegata (na potrzeby serializacji) lub JsonPropertyInfo.Set delegata (na potrzeby deserializacji) dla właściwości . Zwiększanie wartości właściwości
Dodawanie lub usuwanie właściwości JsonTypeInfoKind.Object Dodaj lub usuń elementy z JsonTypeInfo.Properties listy. Serializowanie pól prywatnych
Warunkowe serializowanie właściwości JsonTypeInfoKind.Object Zmodyfikuj JsonPropertyInfo.ShouldSerialize predykat dla właściwości . Ignoruj właściwości o określonym typie
Dostosowywanie obsługi numerów dla określonego typu JsonTypeInfoKind.None Zmodyfikuj JsonTypeInfo.NumberHandling wartość typu. Zezwalaj na wartości int jako ciągi

Przykład: zwiększanie wartości właściwości

Rozważmy następujący przykład, w którym modyfikator zwiększa wartość określonej właściwości w deserializacji, modyfikując jego JsonPropertyInfo.Set delegata. Oprócz definiowania modyfikatora przykład wprowadza również nowy atrybut używany do lokalizowania właściwości, której wartość powinna być zwiększana. Jest to przykład dostosowywania właściwości.

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
        }
    }
}

Zwróć uwagę, że w danych wyjściowych RoundTrips wartość jest zwiększana za każdym razem, Product gdy wystąpienie jest deserializowane.

Przykład: Serializowanie pól prywatnych

Domyślnie System.Text.Json ignoruje pola prywatne i właściwości. W tym przykładzie dodano nowy atrybut dla całej klasy , JsonIncludePrivateFieldsAttributeaby zmienić to ustawienie domyślne. Jeśli modyfikator znajdzie atrybut typu, dodaje wszystkie pola prywatne w typie jako nowe właściwości do 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]
        }
    }
}

Napiwek

Jeśli nazwy pól prywatnych zaczynają się od podkreśleń, rozważ usunięcie podkreśleń z nazw podczas dodawania pól jako nowych właściwości JSON.

Przykład: Ignoruj właściwości o określonym typie

Być może model ma właściwości o określonych nazwach lub typach, których nie chcesz ujawniać użytkownikom. Na przykład może istnieć właściwość, która przechowuje poświadczenia lub niektóre informacje, które są bezużyteczne do stosowania w ładunku.

W poniższym przykładzie pokazano, jak odfiltrować właściwości przy użyciu określonego typu : SecretHolder. Robi to przy użyciu IList<T> metody rozszerzenia, aby usunąć wszystkie właściwości, które mają określony typ z JsonTypeInfo.Properties listy. Odfiltrowane właściwości całkowicie znikają z kontraktu, co oznacza System.Text.Json , że nie są one uwzględniane podczas serializacji lub deserializacji.

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--);
                }
            }
        }
    }
}

Przykład: zezwalanie na wartości int jako ciągi

Być może wejściowy kod JSON może zawierać cudzysłowy wokół jednego z typów liczbowych, ale nie innych. Jeśli masz kontrolę nad klasą, możesz umieścić JsonNumberHandlingAttribute typ, aby rozwiązać ten problem, ale nie. Przed platformą .NET 7 należy napisać konwerter niestandardowy, aby naprawić to zachowanie, co wymaga pisania sporo kodu. Za pomocą dostosowywania kontraktu można dostosować zachowanie obsługi liczb dla dowolnego typu.

Poniższy przykład zmienia zachowanie wszystkich int wartości. Przykład można łatwo dostosować do dowolnego typu lub dla określonej właściwości dowolnego typu.

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)
        }
    }
}

Bez modyfikatora zezwalającego na odczytywanie int wartości z ciągu program zakończyłby się wyjątkiem:

Nieobsługiwany wyjątek. System.Text.Json.JsonException: nie można przekonwertować wartości JSON na System.Int32. Ścieżka: $. X | Numer wiersza: 0 | BytePositionInLine: 9.

Inne sposoby dostosowywania serializacji

Oprócz dostosowywania kontraktu istnieją inne sposoby wpływu na zachowanie serializacji i deserializacji, w tym następujące:

  • Przy użyciu atrybutów pochodzących z JsonAttribute, na przykład JsonIgnoreAttribute i JsonPropertyOrderAttribute.
  • Modyfikując JsonSerializerOptionsna przykład , aby ustawić zasady nazewnictwa lub serializować wartości wyliczenia jako ciągi zamiast liczb.
  • Pisząc konwerter niestandardowy, który wykonuje rzeczywistą pracę zapisu w formacie JSON i podczas deserializacji, konstruowania obiektu.

Dostosowywanie kontraktu jest ulepszeniem tych wstępnie istniejących dostosowań, ponieważ być może nie masz dostępu do typu w celu dodania atrybutów, a pisanie konwertera niestandardowego jest złożone i boli wydajność.

Zobacz też