Comment écrire des convertisseurs personnalisés pour la sérialisation JSON (marshaling) dans .NET

Cet article explique comment créer des convertisseurs personnalisés pour les classes de sérialisation JSON fournies dans l’espace de System.Text.Json noms. Pour une introduction à System.Text.Json , consultez comment sérialiser et désérialiser JSON dans .net.

Un convertisseur est une classe qui convertit un objet ou une valeur vers et à partir de JSON. L' System.Text.Json espace de noms contient des convertisseurs intégrés pour la plupart des types primitifs qui mappent aux primitives JavaScript. Vous pouvez écrire des convertisseurs personnalisés :

  • Pour remplacer le comportement par défaut d’un convertisseur intégré. Par exemple, vous souhaiterez peut-être que les DateTime valeurs soient représentées par le format mm/jj/aaaa. Par défaut, ISO 8601-1:2019 est pris en charge, y compris le profil RFC 3339. Pour plus d’informations, consultez prise en charge des System.Text.Json valeurs DateTime et DateTimeOffset dans .
  • Pour prendre en charge un type valeur personnalisé. Par exemple, un PhoneNumber struct.

Vous pouvez également écrire des convertisseurs personnalisés pour personnaliser ou étendre System.Text.Json des fonctionnalités qui ne sont pas incluses dans la version actuelle. Les scénarios suivants sont abordés plus loin dans cet article :

Dans le code que vous écrivez pour un convertisseur personnalisé, vous devez être conscient de la baisse significative des performances pour l’utilisation de nouvelles JsonSerializerOptions instances. Pour plus d’informations, consultez réutiliser des instances JsonSerializerOptions.

Visual Basic ne peut pas être utilisé pour écrire des convertisseurs personnalisés, mais peut appeler des convertisseurs implémentés dans les bibliothèques C#. pour plus d’informations, consultez prise en charge de Visual Basic.

Modèles de convertisseurs personnalisés

Il existe deux modèles pour créer un convertisseur personnalisé : le modèle de base et le modèle de fabrique. Le modèle de fabrique est destiné aux convertisseurs qui gèrent le type Enum ou les génériques ouverts. Le modèle de base concerne les types génériques non génériques et fermés. Par exemple, les convertisseurs pour les types suivants requièrent le modèle de fabrique :

Voici quelques exemples de types qui peuvent être gérés par le modèle de base :

  • Dictionary<int, string>
  • WeekdaysEnum
  • List<DateTimeOffset>
  • DateTime
  • Int32

Le modèle de base crée une classe qui peut gérer un type. Le modèle de fabrique crée une classe qui détermine, au moment de l’exécution, le type spécifique requis et crée dynamiquement le convertisseur approprié.

Exemple de convertisseur de base

L’exemple suivant est un convertisseur qui remplace la sérialisation par défaut d’un type de données existant. Le convertisseur utilise le format mm/jj/aaaa pour les DateTimeOffset Propriétés.

using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DateTimeOffsetJsonConverter : JsonConverter<DateTimeOffset>
    {
        public override DateTimeOffset Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                DateTimeOffset.ParseExact(reader.GetString(),
                    "MM/dd/yyyy", CultureInfo.InvariantCulture);

        public override void Write(
            Utf8JsonWriter writer,
            DateTimeOffset dateTimeValue,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(dateTimeValue.ToString(
                    "MM/dd/yyyy", CultureInfo.InvariantCulture));
    }
}

Exemple de convertisseur de modèle de fabrique

Le code suivant illustre un convertisseur personnalisé qui fonctionne avec Dictionary<Enum,TValue> . Le code suit le modèle de fabrique, car le premier paramètre de type générique est Enum et le second est ouvert. La CanConvert méthode retourne true uniquement pour un Dictionary avec deux paramètres génériques, le premier étant un Enum type. Le convertisseur interne obtient un convertisseur existant pour gérer le type fourni au moment de l’exécution pour TValue .

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DictionaryTKeyEnumTValueConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType)
            {
                return false;
            }

            if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
            {
                return false;
            }

            return typeToConvert.GetGenericArguments()[0].IsEnum;
        }

        public override JsonConverter CreateConverter(
            Type type,
            JsonSerializerOptions options)
        {
            Type keyType = type.GetGenericArguments()[0];
            Type valueType = type.GetGenericArguments()[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    new Type[] { keyType, valueType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: new object[] { options },
                culture: null);

            return converter;
        }

        private class DictionaryEnumConverterInner<TKey, TValue> :
            JsonConverter<Dictionary<TKey, TValue>> where TKey : struct, Enum
        {
            private readonly JsonConverter<TValue> _valueConverter;
            private readonly Type _keyType;
            private readonly Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter if available.
                _valueConverter = (JsonConverter<TValue>)options
                    .GetConverter(typeof(TValue));

                // Cache the key and value types.
                _keyType = typeof(TKey);
                _valueType = typeof(TValue);
            }

            public override Dictionary<TKey, TValue> Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
            {
                if (reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }

                var dictionary = new Dictionary<TKey, TValue>();

                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndObject)
                    {
                        return dictionary;
                    }

                    // Get the key.
                    if (reader.TokenType != JsonTokenType.PropertyName)
                    {
                        throw new JsonException();
                    }

                    string propertyName = reader.GetString();

                    // For performance, parse with ignoreCase:false first.
                    if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) &&
                        !Enum.TryParse(propertyName, ignoreCase: true, out key))
                    {
                        throw new JsonException(
                            $"Unable to convert \"{propertyName}\" to Enum \"{_keyType}\".");
                    }

                    // Get the value.
                    TValue value;
                    if (_valueConverter != null)
                    {
                        reader.Read();
                        value = _valueConverter.Read(ref reader, _valueType, options);
                    }
                    else
                    {
                        value = JsonSerializer.Deserialize<TValue>(ref reader, options);
                    }

                    // Add to dictionary.
                    dictionary.Add(key, value);
                }

                throw new JsonException();
            }

            public override void Write(
                Utf8JsonWriter writer,
                Dictionary<TKey, TValue> dictionary,
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();

                foreach ((TKey key, TValue value) in dictionary)
                {
                    var propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    if (_valueConverter != null)
                    {
                        _valueConverter.Write(writer, value, options);
                    }
                    else
                    {
                        JsonSerializer.Serialize(writer, value, options);
                    }
                }

                writer.WriteEndObject();
            }
        }
    }
}

Le code précédent est identique à ce qui est affiché dans le dictionnaire de prise en charge avec une clé non-chaîne plus loin dans cet article.

Étapes pour suivre le modèle de base

Les étapes suivantes expliquent comment créer un convertisseur en suivant le modèle de base :

  • Créez une classe qui dérive de JsonConverter<T>T est le type à sérialiser et à désérialiser.
  • Substituez la Read méthode pour désérialiser le JSON entrant et le convertir en type T . Utilisez le Utf8JsonReader passé à la méthode pour lire le JSON. Vous n’avez pas à vous soucier de la gestion des données partielles, car le sérialiseur transmet toutes les données de la portée JSON actuelle. Par conséquent, il n’est pas nécessaire d’appeler Skip ou TrySkip ou de valider que Read retourne true .
  • Substituez la Write méthode pour sérialiser l’objet entrant de type T . Utilisez le Utf8JsonWriter passé à la méthode pour écrire le JSON.
  • Substituez la CanConvert méthode uniquement si nécessaire. L’implémentation par défaut retourne true lorsque le type à convertir est de type T . Par conséquent, les convertisseurs qui prennent en charge uniquement T le type n’ont pas besoin de substituer cette méthode. Pour obtenir un exemple de convertisseur qui doit substituer cette méthode, consultez la section sur la désérialisation polymorphe plus loin dans cet article.

Vous pouvez faire référence au code source des convertisseurs intégrés en tant qu’implémentations de référence pour l’écriture de convertisseurs personnalisés.

Étapes pour suivre le modèle de fabrique

Les étapes suivantes expliquent comment créer un convertisseur en suivant le modèle de fabrique :

  • Créez une classe qui dérive de JsonConverterFactory.
  • Substituez la CanConvert méthode pour retourner la valeur true lorsque le type à convertir est un qui peut être géré par le convertisseur. Par exemple, si le convertisseur est destiné à List<T> lui, il peut uniquement gérer List<int> , List<string> et List<DateTime> .
  • Substituez la CreateConverter méthode pour retourner une instance d’une classe de convertisseur qui gérera le type-conversion fourni au moment de l’exécution.
  • Créez la classe de convertisseur CreateConverter instanciée par la méthode.

Le modèle de fabrique est requis pour les génériques ouverts, car le code permettant de convertir un objet vers et à partir d’une chaîne n’est pas le même pour tous les types. Un convertisseur pour un type générique ouvert ( List<T> , par exemple) doit créer un convertisseur pour un type générique fermé ( List<DateTime> , par exemple) en arrière-plan. Le code doit être écrit pour gérer chaque type de générique fermé que le convertisseur peut gérer.

Le Enum type est similaire à un type générique ouvert : un convertisseur pour Enum doit créer un convertisseur pour un spécifique Enum ( WeekdaysEnum , par exemple) en arrière-plan.

Gestion des erreurs

Le sérialiseur fournit une gestion spéciale pour les types d’exception JsonException et NotSupportedException .

JsonException

Si vous levez une exception JsonException sans message, le sérialiseur crée un message qui comprend le chemin d’accès à la partie du JSON à l’origine de l’erreur. Par exemple, l’instruction throw new JsonException() génère un message d’erreur comme dans l’exemple suivant :

Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.

Si vous fournissez un message (par exemple, throw new JsonException("Error occurred") , le sérialiseur définit toujours les Path LineNumber Propriétés, et BytePositionInLine .

NotSupportedException

Si vous levez une NotSupportedException , vous recevez toujours les informations de chemin d’accès dans le message. Si vous fournissez un message, les informations relatives au chemin d’accès sont ajoutées à celui-ci. Par exemple, l’instruction throw new NotSupportedException("Error occurred.") génère un message d’erreur comme dans l’exemple suivant :

Error occurred. The unsupported member type is located on type
'System.Collections.Generic.Dictionary`2[Samples.SummaryWords,System.Int32]'.
Path: $.TemperatureRanges | LineNumber: 4 | BytePositionInLine: 24

Quand lever le type d’exception

Lorsque la charge utile JSON contient des jetons qui ne sont pas valides pour le type en cours de désérialisation, levez une JsonException .

Lorsque vous souhaitez interdire certains types, levez une NotSupportedException . Cette exception est ce que le sérialiseur lève automatiquement pour les types qui ne sont pas pris en charge. Par exemple, System.Type n’est pas pris en charge pour des raisons de sécurité. par conséquent, une tentative de désérialisation entraîne une NotSupportedException .

Vous pouvez lever d’autres exceptions si nécessaire, mais elles n’incluent pas automatiquement les informations de chemin d’accès JSON.

Inscrire un convertisseur personnalisé

Inscrire un convertisseur personnalisé pour que les Serialize méthodes et l' Deserialize utilisent. Choisissez l’une des approches suivantes :

  • Ajoutez une instance de la classe de convertisseur à la JsonSerializerOptions.Converters collection.
  • Appliquez l’attribut [JsonConverter] aux propriétés qui requièrent le convertisseur personnalisé.
  • Appliquez l’attribut [JsonConverter] à une classe ou un struct qui représente un type valeur personnalisé.

Exemple d’inscription-collection Converters

Voici un exemple qui fait de DateTimeOffsetJsonConverter la valeur par défaut pour les propriétés de type DateTimeOffset :

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true,
    Converters =
    {
        new DateTimeOffsetJsonConverter()
    }
};

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Supposons que vous sérialisez une instance du type suivant :

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

Voici un exemple de sortie JSON qui montre que le convertisseur personnalisé a été utilisé :

{
  "Date": "08/01/2019",
  "TemperatureCelsius": 25,
  "Summary": "Hot"
}

Le code suivant utilise la même approche pour désérialiser à l’aide du DateTimeOffset convertisseur personnalisé :

var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions);

Exemple d’inscription-[JsonConverter] sur une propriété

Le code suivant sélectionne un convertisseur personnalisé pour la Date propriété :

public class WeatherForecastWithConverterAttribute
{
    [JsonConverter(typeof(DateTimeOffsetJsonConverter))]
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

Le code à sérialiser WeatherForecastWithConverterAttribute ne requiert pas l’utilisation de JsonSerializeOptions.Converters :

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Le code à désérialiser ne nécessite pas non plus l’utilisation de Converters :

weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithConverterAttribute>(jsonString);

Exemple d’inscription-[JsonConverter] sur un type

Voici le code qui crée un struct et lui applique l' [JsonConverter] attribut :

using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    [JsonConverter(typeof(TemperatureConverter))]
    public struct Temperature
    {
        public Temperature(int degrees, bool celsius)
        {
            Degrees = degrees;
            IsCelsius = celsius;
        }

        public int Degrees { get; }
        public bool IsCelsius { get; }
        public bool IsFahrenheit => !IsCelsius;

        public override string ToString() =>
            $"{Degrees}{(IsCelsius ? "C" : "F")}";

        public static Temperature Parse(string input)
        {
            int degrees = int.Parse(input.Substring(0, input.Length - 1));
            bool celsius = input.Substring(input.Length - 1) == "C";

            return new Temperature(degrees, celsius);
        }
    }
}

Voici le convertisseur personnalisé pour le struct précédent :

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

namespace SystemTextJsonSamples
{
    public class TemperatureConverter : JsonConverter<Temperature>
    {
        public override Temperature Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                Temperature.Parse(reader.GetString());

        public override void Write(
            Utf8JsonWriter writer,
            Temperature temperature,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(temperature.ToString());
    }
}

L' [JsonConverter] attribut sur le struct inscrit le convertisseur personnalisé comme valeur par défaut pour les propriétés de type Temperature . Le convertisseur est automatiquement utilisé sur la TemperatureCelsius propriété du type suivant lors de la sérialisation ou de la désérialisation de ce dernier :

public class WeatherForecastWithTemperatureStruct
{
    public DateTimeOffset Date { get; set; }
    public Temperature TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

Priorité d’inscription du convertisseur

Lors de la sérialisation ou de la désérialisation, un convertisseur est choisi pour chaque élément JSON dans l’ordre suivant, de la priorité la plus élevée à la plus faible :

  • [JsonConverter] appliqué à une propriété.
  • Convertisseur ajouté à la Converters collection.
  • [JsonConverter] appliqué à un type valeur personnalisé ou POCO.

Si plusieurs convertisseurs personnalisés pour un type sont inscrits dans la Converters collection, le premier convertisseur qui retourne la valeur true pour CanConvert est utilisé.

Un convertisseur intégré est choisi uniquement si aucun convertisseur personnalisé applicable n’est inscrit.

Exemples de convertisseurs pour les scénarios courants

Les sections suivantes fournissent des exemples de convertisseurs qui traitent de certains scénarios courants que les fonctionnalités intégrées ne gèrent pas.

Pour obtenir un exemple de DataTable convertisseur, consultez types de collections pris en charge.

Désérialiser les types inférés en propriétés d’objet

Lors de la désérialisation vers une propriété de type object , un JsonElement objet est créé. Cela est dû au fait que le désérialiseur ne sait pas quel type CLR créer, et qu’il ne tente pas de deviner. Par exemple, si une propriété JSON a la valeur « true », le désérialiseur ne déduit pas que la valeur est un Boolean et, si un élément a « 01/01/2019 », le désérialiseur ne déduit pas qu’il s’agit d’un DateTime .

L’inférence de type peut être inexacte. Si le désérialiseur analyse un nombre JSON qui n’a pas de virgule décimale comme un long , cela peut entraîner des problèmes hors limites si la valeur a été sérialisée à l’origine en tant que ulong ou BigInteger . L’analyse d’un nombre qui a une virgule décimale double peut perdre la précision si le nombre a été initialement sérialisé en tant que decimal .

Pour les scénarios qui requièrent l’inférence de type, le code suivant montre un convertisseur personnalisé pour les object Propriétés. Le code convertit :

  • true et false pour Boolean
  • Nombres sans décimal pour long
  • Nombres avec un nombre décimal à double
  • Dates jusqu’à DateTime
  • Chaînes à string
  • Tout le reste vers JsonElement
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace CustomConverterInferredTypesToObject
{
    public class ObjectToInferredTypesConverter : JsonConverter<object>
    {
        public override object Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) => reader.TokenType switch
            {
                JsonTokenType.True => true,
                JsonTokenType.False => false,
                JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
                JsonTokenType.Number => reader.GetDouble(),
                JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
                JsonTokenType.String => reader.GetString(),
                _ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
            };

        public override void Write(
            Utf8JsonWriter writer,
            object objectToWrite,
            JsonSerializerOptions options) =>
            JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
    }

    public class WeatherForecast
    {
        public object Date { get; set; }
        public object TemperatureCelsius { get; set; }
        public object Summary { get; set; }
    }

    public class Program
    {
        public static void Main()
        {
            string jsonString = @"{
  ""Date"": ""2019-08-01T00:00:00-07:00"",
  ""TemperatureCelsius"": 25,
  ""Summary"": ""Hot""
}";

            WeatherForecast weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString);
            Console.WriteLine($"Type of Date property   no converter = {weatherForecast.Date.GetType()}");

            var options = new JsonSerializerOptions();
            options.WriteIndented = true;
            options.Converters.Add(new ObjectToInferredTypesConverter());
            weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, options);
            Console.WriteLine($"Type of Date property with converter = {weatherForecast.Date.GetType()}");

            Console.WriteLine(JsonSerializer.Serialize(weatherForecast, options));
        }
    }
}

// Produces output like the following example:
//
//Type of Date property   no converter = System.Text.Json.JsonElement
//Type of Date property with converter = System.DateTime
//{
//  "Date": "2019-08-01T00:00:00-07:00",
//  "TemperatureCelsius": 25,
//  "Summary": "Hot"
//}

L’exemple montre le code de convertisseur et une WeatherForecast classe avec des object Propriétés. La Main méthode désérialise une chaîne JSON dans une WeatherForecast instance, tout d’abord sans utiliser le convertisseur, puis en utilisant le convertisseur. La sortie de la console indique que, sans le convertisseur, le type au moment de l’exécution de la Date propriété est JsonElement ; avec le convertisseur, le type au moment de l’exécution est DateTime .

Le dossier tests unitaires de l' System.Text.Json.Serialization espace de noms contient plus d’exemples de convertisseurs personnalisés qui gèrent la désérialisation des object Propriétés.

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

namespace SystemTextJsonSamples
{
    public class ObjectToInferredTypesConverter
        : JsonConverter<object>
    {
        public override object Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) => reader.TokenType switch
            {
                JsonTokenType.True => true,
                JsonTokenType.False => false,
                JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
                JsonTokenType.Number => reader.GetDouble(),
                JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
                JsonTokenType.String => reader.GetString(),
                _ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
            };

        public override void Write(
            Utf8JsonWriter writer,
            object objectToWrite,
            JsonSerializerOptions options) =>
            throw new InvalidOperationException("Should not get here.");
    }
}

Le code suivant inscrit le convertisseur :

var deserializeOptions = new JsonSerializerOptions
{
    Converters =
    {
        new ObjectToInferredTypesConverter()
    }
};

Voici un exemple de type avec des object Propriétés :

public class WeatherForecastWithObjectProperties
{
    public object Date { get; set; }
    public object TemperatureCelsius { get; set; }
    public object Summary { get; set; }
}

L’exemple suivant de JSON à désérialiser contient des valeurs qui seront désérialisées en tant que DateTime , long et string :

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
}

Sans le convertisseur personnalisé, la désérialisation place un JsonElement dans chaque propriété.

Le dossier tests unitaires de l' System.Text.Json.Serialization espace de noms contient plus d’exemples de convertisseurs personnalisés qui gèrent la désérialisation des object Propriétés.

Dictionnaire de prise en charge avec clé non-chaîne

La prise en charge intégrée pour les collections de dictionnaires concerne Dictionary<string, TValue> . Autrement dit, la clé doit être une chaîne. Pour prendre en charge un dictionnaire avec un entier ou un autre type en tant que clé, un convertisseur personnalisé est requis.

Le code suivant illustre un convertisseur personnalisé qui fonctionne avec Dictionary<Enum,TValue> :

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DictionaryTKeyEnumTValueConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType)
            {
                return false;
            }

            if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
            {
                return false;
            }

            return typeToConvert.GetGenericArguments()[0].IsEnum;
        }

        public override JsonConverter CreateConverter(
            Type type,
            JsonSerializerOptions options)
        {
            Type keyType = type.GetGenericArguments()[0];
            Type valueType = type.GetGenericArguments()[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    new Type[] { keyType, valueType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: new object[] { options },
                culture: null);

            return converter;
        }

        private class DictionaryEnumConverterInner<TKey, TValue> :
            JsonConverter<Dictionary<TKey, TValue>> where TKey : struct, Enum
        {
            private readonly JsonConverter<TValue> _valueConverter;
            private readonly Type _keyType;
            private readonly Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter if available.
                _valueConverter = (JsonConverter<TValue>)options
                    .GetConverter(typeof(TValue));

                // Cache the key and value types.
                _keyType = typeof(TKey);
                _valueType = typeof(TValue);
            }

            public override Dictionary<TKey, TValue> Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
            {
                if (reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }

                var dictionary = new Dictionary<TKey, TValue>();

                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndObject)
                    {
                        return dictionary;
                    }

                    // Get the key.
                    if (reader.TokenType != JsonTokenType.PropertyName)
                    {
                        throw new JsonException();
                    }

                    string propertyName = reader.GetString();

                    // For performance, parse with ignoreCase:false first.
                    if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) &&
                        !Enum.TryParse(propertyName, ignoreCase: true, out key))
                    {
                        throw new JsonException(
                            $"Unable to convert \"{propertyName}\" to Enum \"{_keyType}\".");
                    }

                    // Get the value.
                    TValue value;
                    if (_valueConverter != null)
                    {
                        reader.Read();
                        value = _valueConverter.Read(ref reader, _valueType, options);
                    }
                    else
                    {
                        value = JsonSerializer.Deserialize<TValue>(ref reader, options);
                    }

                    // Add to dictionary.
                    dictionary.Add(key, value);
                }

                throw new JsonException();
            }

            public override void Write(
                Utf8JsonWriter writer,
                Dictionary<TKey, TValue> dictionary,
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();

                foreach ((TKey key, TValue value) in dictionary)
                {
                    var propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    if (_valueConverter != null)
                    {
                        _valueConverter.Write(writer, value, options);
                    }
                    else
                    {
                        JsonSerializer.Serialize(writer, value, options);
                    }
                }

                writer.WriteEndObject();
            }
        }
    }
}

Le code suivant inscrit le convertisseur :

var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new DictionaryTKeyEnumTValueConverter());

Le convertisseur peut sérialiser et désérialiser la TemperatureRanges propriété de la classe suivante qui utilise les éléments suivants Enum :

public class WeatherForecastWithEnumDictionary
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
    public Dictionary<SummaryWordsEnum, int> TemperatureRanges { get; set; }
}

public enum SummaryWordsEnum
{
    Cold, Hot
}

La sortie JSON de la sérialisation ressemble à l’exemple suivant :

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
  "TemperatureRanges": {
    "Cold": 20,
    "Hot": 40
  }
}

Le dossier tests unitaires de l' System.Text.Json.Serialization espace de noms contient des exemples de convertisseurs personnalisés qui gèrent des dictionnaires non-clés.

Prendre en charge la désérialisation polymorphe

Les fonctionnalités intégrées offrent une plage limitée de sérialisation polymorphe , mais aucune prise en charge de la désérialisation. La désérialisation requiert un convertisseur personnalisé.

Supposons, par exemple, que vous avez une Person classe de base abstraite, avec des Employee Customer classes dérivées et. La désérialisation polymorphe signifie qu’au moment du design vous pouvez spécifier Person en tant que cible de désérialisation, et les Customer Employee objets et dans le JSON sont correctement désérialisés au moment de l’exécution. Pendant la désérialisation, vous devez trouver des indices qui identifient le type requis dans le JSON. Les types d’indices disponibles varient en fonction de chaque scénario. Par exemple, une propriété de discriminateur peut être disponible ou vous devrez peut-être vous appuyer sur la présence ou l’absence d’une propriété particulière. La version actuelle de System.Text.Json ne fournit pas d’attributs pour spécifier comment gérer les scénarios de désérialisation polymorphe, donc les convertisseurs personnalisés sont requis.

Le code suivant illustre une classe de base, deux classes dérivées et un convertisseur personnalisé pour eux. Le convertisseur utilise une propriété de discriminateur pour effectuer une désérialisation polymorphe. Le discriminateur de type ne figure pas dans les définitions de classe, mais il est créé au cours de la sérialisation et est lu pendant la désérialisation.

public class Person
{
    public string Name { get; set; }
}

public class Customer : Person
{
    public decimal CreditLimit { get; set; }
}

public class Employee : Person
{
    public string OfficeNumber { get; set; }
}
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class PersonConverterWithTypeDiscriminator : JsonConverter<Person>
    {
        enum TypeDiscriminator
        {
            Customer = 1,
            Employee = 2
        }

        public override bool CanConvert(Type typeToConvert) =>
            typeof(Person).IsAssignableFrom(typeToConvert);

        public override Person Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException();
            }

            string propertyName = reader.GetString();
            if (propertyName != "TypeDiscriminator")
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.Number)
            {
                throw new JsonException();
            }

            TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
            Person person = typeDiscriminator switch
            {
                TypeDiscriminator.Customer => new Customer(),
                TypeDiscriminator.Employee => new Employee(),
                _ => throw new JsonException()
            };

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    return person;
                }

                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    propertyName = reader.GetString();
                    reader.Read();
                    switch (propertyName)
                    {
                        case "CreditLimit":
                            decimal creditLimit = reader.GetDecimal();
                            ((Customer)person).CreditLimit = creditLimit;
                            break;
                        case "OfficeNumber":
                            string officeNumber = reader.GetString();
                            ((Employee)person).OfficeNumber = officeNumber;
                            break;
                        case "Name":
                            string name = reader.GetString();
                            person.Name = name;
                            break;
                    }
                }
            }

            throw new JsonException();
        }

        public override void Write(
            Utf8JsonWriter writer, Person person, JsonSerializerOptions options)
        {
            writer.WriteStartObject();

            if (person is Customer customer)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Customer);
                writer.WriteNumber("CreditLimit", customer.CreditLimit);
            }
            else if (person is Employee employee)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Employee);
                writer.WriteString("OfficeNumber", employee.OfficeNumber);
            }

            writer.WriteString("Name", person.Name);

            writer.WriteEndObject();
        }
    }
}

Le code suivant inscrit le convertisseur :

var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new PersonConverterWithTypeDiscriminator());

Le convertisseur peut désérialiser JSON qui a été créé à l’aide du même convertisseur pour sérialiser, par exemple :

[
  {
    "TypeDiscriminator": 1,
    "CreditLimit": 10000,
    "Name": "John"
  },
  {
    "TypeDiscriminator": 2,
    "OfficeNumber": "555-1234",
    "Name": "Nancy"
  }
]

Dans l’exemple précédent, le code de convertisseur lit et écrit chaque propriété manuellement. Une alternative consiste à appeler Deserialize ou Serialize à effectuer une partie du travail. Pour obtenir un exemple, consultez cette publication StackOverflow.

Une autre façon d’effectuer une désérialisation polymorphe

Vous pouvez appeler Deserialize dans la Read méthode :

  • Créez un clone de l' Utf8JsonReader instance. Étant donné que Utf8JsonReader est un struct, cela nécessite simplement une instruction d’assignation.
  • Utilisez le clone pour lire les jetons de discriminateur.
  • Appelez Deserialize à l’aide de l’instance d’origine Reader une fois que vous connaissez le type dont vous avez besoin. Vous pouvez appeler Deserialize parce que l' Reader instance d’origine est toujours positionnée pour lire le jeton de début de l’objet.

L’inconvénient de cette méthode est que vous ne pouvez pas passer l’instance d’options d’origine qui inscrit le convertisseur à Deserialize . Cela entraînerait un dépassement de capacité de la pile, comme expliqué dans les propriétés requises. L’exemple suivant montre une Read méthode qui utilise cette alternative :

public override Person Read(
    ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    Utf8JsonReader readerClone = reader;

    if (readerClone.TokenType != JsonTokenType.StartObject)
    {
        throw new JsonException();
    }

    readerClone.Read();
    if (readerClone.TokenType != JsonTokenType.PropertyName)
    {
        throw new JsonException();
    }

    string propertyName = readerClone.GetString();
    if (propertyName != "TypeDiscriminator")
    {
        throw new JsonException();
    }

    readerClone.Read();
    if (readerClone.TokenType != JsonTokenType.Number)
    {
        throw new JsonException();
    }

    TypeDiscriminator typeDiscriminator = (TypeDiscriminator)readerClone.GetInt32();
    Person person = typeDiscriminator switch
    {
        TypeDiscriminator.Customer => JsonSerializer.Deserialize<Customer>(ref reader),
        TypeDiscriminator.Employee => JsonSerializer.Deserialize<Employee>(ref reader),
        _ => throw new JsonException()
    };
    return person;
}

Aller-retour du support pour la pile<T>

Si vous désérialisez une chaîne JSON dans un Stack<T> objet et que vous sérialisez ensuite cet objet, le contenu de la pile est dans l’ordre inverse. Ce comportement s’applique aux types et à l’interface suivants, ainsi qu’aux types définis par l’utilisateur qui dérivent de ceux-ci :

Pour prendre en charge la sérialisation et la désérialisation qui conservent l’ordre d’origine dans la pile, un convertisseur personnalisé est requis.

Le code suivant illustre un convertisseur personnalisé qui permet l’aller-retour vers et à partir d' Stack<T> objets :

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class JsonConverterFactoryForStackOfT : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
            => typeToConvert.IsGenericType
            && typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>);

        public override JsonConverter CreateConverter(
            Type typeToConvert, JsonSerializerOptions options)
        {
            Debug.Assert(typeToConvert.IsGenericType &&
                typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>));

            Type elementType = typeToConvert.GetGenericArguments()[0];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(JsonConverterForStackOfT<>)
                    .MakeGenericType(new Type[] { elementType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: null,
                culture: null)!;

            return converter;
        }
    }

    public class JsonConverterForStackOfT<T> : JsonConverter<Stack<T>>
    {
        public override Stack<T> Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartArray)
            {
                throw new JsonException();
            }
            reader.Read();

            var elements = new Stack<T>();

            while (reader.TokenType != JsonTokenType.EndArray)
            {
                elements.Push(JsonSerializer.Deserialize<T>(ref reader, options));

                reader.Read();
            }

            return elements;
        }

        public override void Write(
            Utf8JsonWriter writer, Stack<T> value, JsonSerializerOptions options)
        {
            writer.WriteStartArray();

            var reversed = new Stack<T>(value);

            foreach (T item in reversed)
            {
                JsonSerializer.Serialize(writer, item, options);
            }

            writer.WriteEndArray();
        }
    }
}

Le code suivant inscrit le convertisseur :

var options = new JsonSerializerOptions
{
    Converters = { new JsonConverterFactoryForStackOfT() },
};

Traiter les valeurs Null

Par défaut, le sérialiseur gère les valeurs NULL comme suit :

  • Pour les types et types référence Nullable<T> :

    • Elle ne passe pas null aux convertisseurs personnalisés lors de la sérialisation.
    • Elle ne passe pas JsonTokenType.Null aux convertisseurs personnalisés lors de la désérialisation.
    • Elle retourne une null instance lors de la désérialisation.
    • Il écrit null directement avec le writer sur la sérialisation.
  • Pour les types valeur n’acceptant pas les valeurs NULL :

    • Il passe JsonTokenType.Null aux convertisseurs personnalisés lors de la désérialisation. (Si aucun convertisseur personnalisé n’est disponible, une JsonException exception est levée par le convertisseur interne pour le type.)

Ce comportement de gestion null est principalement pour optimiser les performances en ignorant un appel supplémentaire au convertisseur. En outre, il évite les convertisseurs de forçage pour les types Nullable à vérifier null au début de chaque Read Write remplacement de méthode et.

Pour permettre à un convertisseur personnalisé de gérer null un type référence ou valeur, remplacez JsonConverter<T>.HandleNull pour retourner true , comme indiqué dans l’exemple suivant :

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

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

        [JsonConverter(typeof(DescriptionConverter))]
        public string Description { get; set; }
    }

    public class DescriptionConverter : JsonConverter<string>
    {
        public override bool HandleNull => true;

        public override string Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
            reader.GetString() ?? "No description provided.";

        public override void Write(
            Utf8JsonWriter writer,
            string value,
            JsonSerializerOptions options) =>
            writer.WriteStringValue(value);
    }

    public class Program
    {
        public static void Main()
        {
            string json = @"{""x"":1,""y"":2,""Description"":null}";

            Point point = JsonSerializer.Deserialize<Point>(json);
            Console.WriteLine($"Description: {point.Description}");
        }
    }
}

// Produces output like the following example:
//
//Description: No description provided.

Préserver les références

Par défaut, les données de référence sont mises en cache uniquement pour chaque appel à Serialize ou Deserialize . Pour conserver les références d’un Serialize / Deserialize appel à un autre, racine de l' ReferenceResolver instance dans le site d’appel de Serialize / Deserialize . Le code suivant illustre un exemple de ce scénario :

  • Vous écrivez un convertisseur personnalisé pour le Company type.
  • Vous ne souhaitez pas sérialiser manuellement la Supervisor propriété, qui est un Employee . Vous souhaitez la déléguer au sérialiseur et vous souhaitez également conserver les références que vous avez déjà enregistrées.

Voici les Employee classes et Company :

public class Employee
{
    public string Name { get; set; }
    public Employee Manager { get; set; }
    public List<Employee> DirectReports { get; set; }
    public Company Company { get; set; }
}

public class Company
{
    public string Name { get; set; }
    public Employee Supervisor { get; set; }
}

Le convertisseur se présente comme suit :

class CompanyConverter : JsonConverter<Company>
{
    public override Company Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, Company value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        writer.WriteString("Name", value.Name);

        writer.WritePropertyName("Supervisor");
        JsonSerializer.Serialize(writer, value.Supervisor, options);

        writer.WriteEndObject();
    }
}

Une classe qui dérive de ReferenceResolver stocke les références dans un dictionnaire :

class MyReferenceResolver : ReferenceResolver
{
    private uint _referenceCount;
    private readonly Dictionary<string, object> _referenceIdToObjectMap = new ();
    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;
    }
}

Une classe qui dérive de ReferenceHandler contient une instance de MyReferenceResolver et crée une nouvelle instance uniquement lorsque cela est nécessaire (dans une méthode nommée Reset dans cet exemple) :

class MyReferenceHandler : ReferenceHandler
{
    public MyReferenceHandler() => Reset();

    private ReferenceResolver _rootedResolver;
    public override ReferenceResolver CreateResolver() => _rootedResolver;
    public void Reset() => _rootedResolver = new MyReferenceResolver();

}

Lorsque l’exemple de code appelle le sérialiseur, il utilise une JsonSerializerOptions instance dans laquelle la ReferenceHandler propriété est définie sur une instance de MyReferenceHandler . Lorsque vous suivez ce modèle, veillez à réinitialiser le ReferenceResolver dictionnaire lorsque vous avez fini de sérialiser, afin qu’il ne soit pas toujours croissant.

var options = new JsonSerializerOptions();

options.Converters.Add(new CompanyConverter());
var myReferenceHandler = new MyReferenceHandler();
options.ReferenceHandler = myReferenceHandler;
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.WriteIndented = true;

string str = JsonSerializer.Serialize(tyler, options);

// Reset after serializing to avoid out of bounds memory growth in the resolver.
myReferenceHandler.Reset();

L’exemple précédent n’effectue que la sérialisation, mais une approche similaire peut être adoptée pour la désérialisation.

Pour plus d’informations sur la façon de conserver les références, consultez la version .net 5,0 de cette page.

Autres exemples de convertisseurs personnalisés

L’article migrer de Newtonsoft.Json vers System.Text.Json contient des exemples supplémentaires de convertisseurs personnalisés.

Le dossier tests unitaires du System.Text.Json.Serialization code source comprend d’autres exemples de convertisseurs personnalisés, tels que :

Si vous devez créer un convertisseur qui modifie le comportement d’un convertisseur intégré existant, vous pouvez faire en sorte que le code source du convertisseur existant serve de point de départ pour la personnalisation.

Ressources supplémentaires