Partager via


Personnaliser un contrat JSON

La bibliothèque System.Text.Json construit un contrat JSON pour chaque type .NET, qui définit la façon dont le type doit être sérialisé et désérialisé. Le contrat est dérivé de la forme du type, qui inclut des caractéristiques comme ses propriétés et ses champs et s’il implémente l’interface IEnumerable ou IDictionary. Les types sont mappés à des contrats au moment de l’exécution à l’aide de la réflexion, ou au moment de la compilation à l’aide du générateur source.

À compter de .NET 7, vous pouvez personnaliser ces contrats JSON pour mieux contrôler la façon dont les types sont convertis en JSON et vice versa. La liste suivante ne présente que quelques exemples des types de personnalisations que vous pouvez effectuer pour la sérialisation et la désérialisation :

  • Sérialiser les propriétés et champs privés.
  • Prenez en charge plusieurs noms pour une seule propriété (par exemple, si une version de bibliothèque précédente utilisait un nom différent).
  • Ignorez les propriétés avec un nom, un type ou une valeur spécifique.
  • Faites la distinction entre les valeurs null explicites et l’absence de valeur dans la charge utile JSON.
  • Prise en charge des attributs System.Runtime.Serialization, comme DataContractAttribute. Pour plus d’informations, consultez Attributs System.Runtime.Serialization.
  • Lève une exception si le code JSON inclut une propriété qui ne fait pas partie du type cible. Pour plus d’informations, consultez Gérer les membres manquants.

Comment activer la fonctionnalité

Il existe deux façons d’utiliser la personnalisation. Les deux impliquent l’obtention d’un résolveur, dont le travail consiste à fournir une instance JsonTypeInfo pour chaque type qui doit être sérialisé.

  • En appelant le constructeur DefaultJsonTypeInfoResolver() pour obtenir le JsonSerializerOptions.TypeInfoResolver et en ajoutant vos actions personnalisées à sa propriété Modifiers.

    Par exemple :

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

    Si vous ajoutez plusieurs modificateurs, ils sont appelés séquentiellement.

  • En écrivant un résolveur personnalisé qui implémente IJsonTypeInfoResolver.

    • Si un type n’est pas géré, IJsonTypeInfoResolver.GetTypeInfo doit retourner null pour ce type.
    • Vous pouvez également combiner votre résolveur personnalisé avec d’autres, par exemple le résolveur par défaut. Les résolveurs sont interrogés dans l’ordre jusqu’à ce qu’une valeur JsonTypeInfo non null soit retournée pour le type.

Aspects configurables

La propriété JsonTypeInfo.Kind indique comment le convertisseur sérialise un type donné, par exemple en tant qu’objet ou en tant que tableau et si ses propriétés sont sérialisées. Vous pouvez interroger cette propriété pour déterminer les aspects du contrat JSON d’un type que vous pouvez configurer. Il existe quatre types différents :

JsonTypeInfo.Kind Description
JsonTypeInfoKind.Object Le convertisseur sérialise le type dans un objet JSON et utilise ses propriétés. Ce type est utilisé pour la plupart des types class et struct et offre une flexibilité maximale.
JsonTypeInfoKind.Enumerable Le convertisseur sérialise le type dans un tableau JSON. Ce type est utilisé pour les types comme List<T> et tableau.
JsonTypeInfoKind.Dictionary Le convertisseur sérialise le type dans un objet JSON. Ce type est utilisé pour les types comme Dictionary<K, V>.
JsonTypeInfoKind.None Le convertisseur ne spécifie pas comment il sérialise le type ni les propriétés JsonTypeInfo qu’il utilisera. Ce type est utilisé pour les types comme System.Object, int et string, et pour tous les types qui utilisent un convertisseur personnalisé.

Modificateurs

Un modificateur est un Action<JsonTypeInfo> ou une méthode avec un paramètre JsonTypeInfo qui obtient l’état actuel du contrat en tant qu’argument et apporte des modifications au contrat. Par exemple, vous pouvez effectuer une itération dans les propriétés préremplies sur le JsonTypeInfo spécifié pour trouver celle qui vous intéresse, puis modifier sa propriété JsonPropertyInfo.Get (pour la sérialisation) ou JsonPropertyInfo.Set (pour la désérialisation). Vous pouvez également construire une nouvelle propriété à l’aide de JsonTypeInfo.CreateJsonPropertyInfo(Type, String) et l’ajouter à la collection JsonTypeInfo.Properties.

Le tableau suivant présente les modifications que vous pouvez apporter et comment les réaliser.

Modification JsonTypeInfo.Kind applicable Comment y parvenir Exemple
Personnaliser la valeur d’une propriété JsonTypeInfoKind.Object Modifiez le délégué JsonPropertyInfo.Get (pour la sérialisation) ou JsonPropertyInfo.Set (pour la désérialisation) pour la propriété. Incrémenter une valeur de propriété
Ajouter ou supprimer des propriétés JsonTypeInfoKind.Object Ajouter ou supprimer des éléments dans la liste JsonTypeInfo.Properties. Sérialiser des champs privés
Sérialiser une propriété de manière conditionnelle JsonTypeInfoKind.Object Modifiez le prédicat JsonPropertyInfo.ShouldSerialize de la propriété. Ignorer les propriétés avec un type spécifique
Personnaliser la gestion des nombres pour un type spécifique JsonTypeInfoKind.None Modifiez la valeur JsonTypeInfo.NumberHandling du type. Autoriser les valeurs int à être des chaînes

Exemple : Incrémenter la valeur d’une propriété

Prenons l’exemple suivant où le modificateur incrémente la valeur d’une certaine propriété lors de la désérialisation en modifiant son délégué JsonPropertyInfo.Set. Outre la définition du modificateur, l’exemple introduit un nouvel attribut qu’il utilise pour localiser la propriété dont la valeur doit être incrémentée. Il s’agit d’un exemple de personnalisation d’une propriété.

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

Notez dans la sortie que la valeur de RoundTrips est incrémentée chaque fois que l’instance Product est désérialisée.

Exemple : Sérialiser des champs privés

Par défaut, System.Text.Json ignore les propriétés et champs privés. Cet exemple ajoute un nouvel attribut à l’échelle de la classe, JsonIncludePrivateFieldsAttribute, pour modifier cette valeur par défaut. Si le modificateur trouve l’attribut sur un type, il ajoute tous les champs privés du type en tant que nouvelles propriétés à 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]
        }
    }
}

Conseil

Si vos noms de champs privés commencent par des traits de soulignement, envisagez de supprimer les traits de soulignement des noms lorsque vous ajoutez les champs en tant que nouvelles propriétés JSON.

Exemple : Ignorer les propriétés avec un type spécifique

Votre modèle a peut-être des propriétés avec des noms ou des types spécifiques que vous ne souhaitez pas exposer aux utilisateurs. Par exemple, vous pouvez avoir une propriété qui stocke des informations d’identification ou des informations inutiles à avoir dans la charge utile.

L’exemple suivant montre comment filtrer des propriétés avec un type spécifique, SecretHolder. Pour ce faire, il utilise une méthode d’extension IList<T> pour supprimer de la liste JsonTypeInfo.Properties toutes les propriétés qui ont le type spécifié. Les propriétés filtrées disparaissent complètement du contrat, ce qui signifie que System.Text.Json ne les examine pas pendant la sérialisation ou la désérialisation.

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

Exemple : Autoriser les valeurs int à être des chaînes

Peut-être que votre JSON d’entrée peut contenir des guillemets autour de l’un des types numériques, mais pas pour les autres. Si vous aviez le contrôle sur la classe, vous pourriez placer JsonNumberHandlingAttribute sur le type pour résoudre ce problème, mais ce n’est pas le cas. Avant .NET 7, vous deviez écrire un convertisseur personnalisé pour corriger ce comportement, ce qui nécessite d’écrire un peu de code. À l’aide de la personnalisation du contrat, vous pouvez personnaliser le comportement de gestion des nombres pour n’importe quel type.

L’exemple suivant modifie le comportement pour toutes les valeurs int. L’exemple peut être facilement ajusté pour s’appliquer à n’importe quel type ou à une propriété spécifique de n’importe quel type.

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

Sans le modificateur permettant de lire des valeurs int à partir d’une chaîne, le programme se serait terminé avec une exception :

Exception non gérée. System.Text.Json.JsonException : la valeur JSON n’a pas pu être convertie en System.Int32. Path: $.X | LineNumber: 0 | BytePositionInLine: 9.

Autres façons de personnaliser la sérialisation

Outre la personnalisation d’un contrat, il existe d’autres façons d’influencer le comportement de sérialisation et de désérialisation, notamment les suivantes :

  • En utilisant des attributs dérivés de JsonAttribute, par exemple, JsonIgnoreAttribute et JsonPropertyOrderAttribute.
  • En modifiant JsonSerializerOptions, par exemple, pour définir une stratégie de nommage ou sérialiser des valeurs d’énumération sous forme de chaînes au lieu de nombres.
  • En écrivant un convertisseur personnalisé qui effectue le travail réel d’écriture du JSON et, pendant la désérialisation, la construction d’un objet.

La personnalisation du contrat est une amélioration par rapport à ces personnalisations préexistantes, car vous n’avez peut-être pas accès au type pour ajouter des attributs, et l’écriture d’un convertisseur personnalisé est complexe et nuit aux performances.

Voir aussi