Come scrivere convertitori personalizzati per la serializzazione JSON (marshalling) in .NET

Questo articolo illustra come creare convertitori personalizzati per le classi di serializzazione JSON fornite nello spazio dei System.Text.Json nomi . Per un'introduzione a System.Text.Json, vedere Come serializzare e deserializzare JSON in .NET.

Un convertitore è una classe che converte un oggetto o un valore in e da JSON. Lo System.Text.Json spazio dei nomi include convertitori predefiniti per la maggior parte dei tipi primitivi che eseguono il mapping alle primitive JavaScript. È possibile scrivere convertitori personalizzati per eseguire l'override del comportamento predefinito di un convertitore predefinito. Ad esempio:

  • È possibile che DateTime i valori siano rappresentati dal formato mm/gg/aaaa. Per impostazione predefinita, è supportato ISO 8601-1:2019, incluso il profilo RFC 3339. Per altre informazioni, vedere Supporto di DateTime e DateTimeOffset in System.Text.Json.
  • È possibile serializzare un POCO come stringa JSON, ad esempio con un PhoneNumber tipo .

È anche possibile scrivere convertitori personalizzati per personalizzare o estendere System.Text.Json con nuove funzionalità. Gli scenari seguenti sono illustrati più avanti in questo articolo:

Visual Basic non può essere usato per scrivere convertitori personalizzati, ma può chiamare convertitori implementati nelle librerie C#. Per altre informazioni, vedere Supporto di Visual Basic.

Modelli di convertitore personalizzati

Esistono due modelli per la creazione di un convertitore personalizzato: il modello di base e il modello factory. Il modello factory è destinato ai convertitori che gestiscono il tipo Enum o i generics aperti. Il modello di base è per i tipi generici non generici e chiusi. Ad esempio, i convertitori per i tipi seguenti richiedono il modello factory:

Alcuni esempi di tipi che possono essere gestiti dal modello di base includono:

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

Il modello di base crea una classe in grado di gestire un tipo. Il modello factory crea una classe che determina, in fase di esecuzione, quale tipo specifico è necessario e crea dinamicamente il convertitore appropriato.

Convertitore di base di esempio

L'esempio seguente è un convertitore che esegue l'override della serializzazione predefinita per un tipo di dati esistente. Il convertitore usa il formato mm/gg/aaa per DateTimeOffset le proprietà.

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

Convertitore di modelli factory di esempio

Il codice seguente illustra un convertitore personalizzato che funziona con Dictionary<Enum,TValue>. Il codice segue il modello factory perché il primo parametro di tipo generico è Enum e il secondo è aperto. Il CanConvert metodo restituisce true solo per un Dictionary oggetto con due parametri generici, il primo dei quali è un Enum tipo. Il convertitore interno ottiene un convertitore esistente per gestire qualsiasi tipo venga fornito in fase di esecuzione per TValue.

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[] typeArguments = type.GetGenericArguments();
            Type keyType = typeArguments[0];
            Type valueType = typeArguments[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    [keyType, valueType]),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: [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.
                _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.
                    reader.Read();
                    TValue value = _valueConverter.Read(ref reader, _valueType, 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)
                {
                    string propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    _valueConverter.Write(writer, value, options);
                }

                writer.WriteEndObject();
            }
        }
    }
}

Passaggi per seguire il modello di base

I passaggi seguenti illustrano come creare un convertitore seguendo il modello di base:

  • Creare una classe che deriva da JsonConverter<T> dove T è il tipo da serializzare e deserializzare.
  • Eseguire l'override del Read metodo per deserializzare il codice JSON in ingresso e convertirlo in tipo T. Usare l'oggetto Utf8JsonReader passato al metodo per leggere il codice JSON. Non è necessario preoccuparsi della gestione dei dati parziali, perché il serializzatore passa tutti i dati per l'ambito JSON corrente. Non è quindi necessario chiamare Skip o TrySkip per convalidare che Read restituisca true.
  • Eseguire l'override del Write metodo per serializzare l'oggetto in ingresso di tipo T. Usare l'oggetto Utf8JsonWriter passato al metodo per scrivere il codice JSON.
  • Eseguire l'override del CanConvert metodo solo se necessario. L'implementazione predefinita restituisce true quando il tipo da convertire è di tipo T. Pertanto, i convertitori che supportano solo il tipo T non devono eseguire l'override di questo metodo. Per un esempio di convertitore che deve eseguire l'override di questo metodo, vedere la sezione di deserializzazione polimorfica più avanti in questo articolo.

È possibile fare riferimento al codice sorgente dei convertitori predefiniti come implementazioni di riferimento per la scrittura di convertitori personalizzati.

Passaggi per seguire il modello factory

I passaggi seguenti illustrano come creare un convertitore seguendo il modello factory:

  • Creare una classe che deriva da JsonConverterFactory.
  • Eseguire l'override del CanConvert metodo da restituire true quando il tipo da convertire è uno che il convertitore può gestire. Ad esempio, se il convertitore è per List<T>, potrebbe gestire List<int>solo , List<string>e List<DateTime>.
  • Eseguire l'override del CreateConverter metodo per restituire un'istanza di una classe convertitore che gestirà il tipo da convertire fornito in fase di esecuzione.
  • Creare la classe del convertitore creata dall'istanza del CreateConverter metodo .

Il modello factory è necessario per i generics aperti perché il codice per convertire un oggetto in e da una stringa non è lo stesso per tutti i tipi. Un convertitore per un tipo generico aperto (List<T>ad esempio) deve creare un convertitore per un tipo generico chiuso (List<DateTime>ad esempio) dietro le quinte. Il codice deve essere scritto per gestire ogni tipo generico chiuso che il convertitore può gestire.

Il Enum tipo è simile a un tipo generico aperto: un convertitore per Enum deve creare un convertitore per uno specifico Enum (WeekdaysEnumad esempio) dietro le quinte.

Uso di Utf8JsonReader nel Read metodo

Se il convertitore converte un oggetto JSON, l'oggetto Utf8JsonReader verrà posizionato sul token dell'oggetto iniziale all'inizio del Read metodo. È quindi necessario leggere tutti i token in tale oggetto e uscire dal metodo con il lettore posizionato sul token dell'oggetto finale corrispondente. Se si legge oltre la fine dell'oggetto o si arresta prima di raggiungere il token finale corrispondente, si ottiene un'eccezione JsonException che indica che:

Il convertitore 'ConverterName' legge troppo o non abbastanza.

Per un esempio, vedere il convertitore di esempio del modello factory precedente. Il Read metodo inizia verificando che il lettore sia posizionato su un token dell'oggetto iniziale. Legge fino a quando non rileva che è posizionato sul token dell'oggetto finale successivo. Si arresta sul token dell'oggetto finale successivo perché non sono presenti token di oggetto iniziale che indicano un oggetto all'interno dell'oggetto. La stessa regola relativa al token di inizio e al token finale si applica se si sta convertendo una matrice. Per un esempio, vedere il Stack<T> convertitore di esempio più avanti in questo articolo.

Gestione degli errori

Il serializzatore fornisce una gestione speciale per i tipi di JsonException eccezione e NotSupportedException.

JsonException

Se si genera un JsonException oggetto senza un messaggio, il serializzatore crea un messaggio che include il percorso della parte del codice JSON che ha causato l'errore. Ad esempio, l'istruzione throw new JsonException() genera un messaggio di errore simile all'esempio seguente:

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

Se si specifica un messaggio , ad esempio , throw new JsonException("Error occurred")il serializzatore imposta comunque le Pathproprietà , LineNumbere BytePositionInLine .

NotSupportedException

Se si genera un'eccezione NotSupportedException, si ottengono sempre le informazioni sul percorso nel messaggio. Se si specifica un messaggio, le informazioni sul percorso vengono aggiunte. Ad esempio, l'istruzione throw new NotSupportedException("Error occurred.") genera un messaggio di errore simile all'esempio seguente:

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

Quando generare il tipo di eccezione

Quando il payload JSON contiene token non validi per il tipo da deserializzare, generare un'eccezione JsonException.

Quando si desidera impedire determinati tipi, generare un'eccezione NotSupportedException. Questa eccezione è l'eccezione generata automaticamente dal serializzatore per i tipi non supportati. Ad esempio, System.Type non è supportato per motivi di sicurezza, quindi un tentativo di deserializzazione comporta un oggetto NotSupportedException.

È possibile generare altre eccezioni in base alle esigenze, ma non includono automaticamente informazioni sul percorso JSON.

Registrare un convertitore personalizzato

Registrare un convertitore personalizzato per usare i Serialize metodi e Deserialize . Scegliere uno degli approcci seguenti:

  • Aggiungere un'istanza della classe del convertitore alla JsonSerializerOptions.Converters raccolta.
  • Applicare l'attributo [JsonConverter] alle proprietà che richiedono il convertitore personalizzato.
  • Applicare l'attributo [JsonConverter] a una classe o a uno struct che rappresenta un tipo di valore personalizzato.

Esempio di registrazione - Insieme Converters

Ecco un esempio che rende DateTimeOffsetJsonConverter l'impostazione predefinita per le proprietà di tipo DateTimeOffset:

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

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Si supponga di serializzare un'istanza del tipo seguente:

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

Ecco un esempio di output JSON che mostra che è stato usato il convertitore personalizzato:

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

Il codice seguente usa lo stesso approccio per deserializzare usando il convertitore personalizzato DateTimeOffset :

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

Esempio di registrazione - [JsonConverter] in una proprietà

Il codice seguente seleziona un convertitore personalizzato per la Date proprietà :

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

Il codice da serializzare WeatherForecastWithConverterAttribute non richiede l'uso di JsonSerializeOptions.Converters:

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

Il codice da deserializzare non richiede anche l'uso di Converters:

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

Esempio di registrazione - [JsonConverter] in un tipo

Ecco il codice che crea uno struct e applica l'attributo [JsonConverter] a esso:

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

Ecco il convertitore personalizzato per lo struct precedente:

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'attributo [JsonConverter] nello struct registra il convertitore personalizzato come predefinito per le proprietà di tipo Temperature. Il convertitore viene utilizzato automaticamente nella TemperatureCelsius proprietà del tipo seguente quando si serializza o si deserializza:

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

Precedenza della registrazione del convertitore

Durante la serializzazione o la deserializzazione, viene scelto un convertitore per ogni elemento JSON nell'ordine seguente, elencato dalla priorità più alta al più basso:

  • [JsonConverter] applicato a una proprietà.
  • Convertitore aggiunto alla Converters raccolta.
  • [JsonConverter] applicato a un tipo di valore personalizzato o POCO.

Se nella raccolta vengono registrati Converters più convertitori personalizzati per un tipo, viene utilizzato il primo convertitore che restituisce true per CanConvert .

Viene scelto un convertitore predefinito solo se non viene registrato alcun convertitore personalizzato applicabile.

Esempi di convertitori per scenari comuni

Le sezioni seguenti forniscono esempi di convertitore che riguardano alcuni scenari comuni che non gestiscono le funzionalità predefinite.

Per un convertitore di esempio DataTable , vedere Tipi di raccolta supportati.

Deserializzare i tipi dedotti alle proprietà dell'oggetto

Quando si deserializzazione in una proprietà di tipo object, viene creato un JsonElement oggetto . Il motivo è che il deserializzatore non conosce il tipo CLR da creare e non tenta di indovinare. Ad esempio, se una proprietà JSON ha "true", il deserializzatore non deduce che il valore è un Booleane se un elemento ha "01/01/2019", il deserializzatore non deduce che si tratta di un oggetto DateTime.

L'inferenza del tipo può essere imprecisa. Se il deserializzatore analizza un numero JSON che non dispone di un separatore decimale come long, che potrebbe causare problemi di out-of-range se il valore è stato originariamente serializzato come o ulongBigInteger. L'analisi di un numero con un separatore decimale come double può perdere precisione se il numero è stato originariamente serializzato come .decimal

Per gli scenari che richiedono l'inferenza del tipo, il codice seguente mostra un convertitore personalizzato per object le proprietà. Il codice converte:

  • true e false a Boolean
  • Numeri senza decimale a long
  • Numeri con un separatore decimale a double
  • Date a DateTime
  • Stringhe a string
  • Tutto il resto JsonElement
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'esempio mostra il codice del convertitore e una WeatherForecast classe con object proprietà. Il Main metodo deserializza una stringa JSON in un'istanza WeatherForecast , prima senza usare il convertitore e quindi usando il convertitore. L'output della console mostra che senza il convertitore, il tipo di runtime per la Date proprietà è JsonElement. Con il convertitore, il tipo di runtime è DateTime.

La cartella unit test nello System.Text.Json.Serialization spazio dei nomi include altri esempi di convertitori personalizzati che gestiscono la deserializzazione alle object proprietà.

Supportare la deserializzazione polimorfica

.NET 7 offre supporto sia per la serializzazione polimorfica che per la deserializzazione. Tuttavia, nelle versioni precedenti di .NET era disponibile un supporto limitato per la serializzazione polimorfica e nessun supporto per la deserializzazione. Se si usa .NET 6 o una versione precedente, la deserializzazione richiede un convertitore personalizzato.

Si supponga, ad esempio, di avere una Person classe base astratta con Employee e Customer classi derivate. La deserializzazione polimorfica significa che in fase di progettazione è possibile specificare Person come destinazione di deserializzazione e Customer gli Employee oggetti nel codice JSON vengono deserializzati correttamente in fase di esecuzione. Durante la deserializzazione, è necessario trovare indizi che identificano il tipo richiesto nel codice JSON. I tipi di indizi disponibili variano in base a ogni scenario. Ad esempio, una proprietà discriminatoria potrebbe essere disponibile o potrebbe essere necessario basarsi sulla presenza o sull'assenza di una determinata proprietà. La versione corrente di System.Text.Json non fornisce attributi per specificare come gestire scenari di deserializzazione polimorfica, quindi sono necessari convertitori personalizzati.

Il codice seguente mostra una classe base, due classi derivate e un convertitore personalizzato per tali classi. Il convertitore usa una proprietà discriminatoria per eseguire la deserializzazione polimorfica. Il discriminare del tipo non è presente nelle definizioni di classe, ma viene creato durante la serializzazione e viene letto durante la deserializzazione.

Importante

Il codice di esempio richiede che le coppie nome/valore dell'oggetto JSON rimangano in ordine, che non è un requisito standard di JSON.

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

Il codice seguente registra il convertitore:

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

Il convertitore può deserializzare JSON creato usando lo stesso convertitore per serializzare, ad esempio:

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

Il codice del convertitore nell'esempio precedente legge e scrive ogni proprietà manualmente. Un'alternativa consiste nel chiamare Deserialize o Serialize eseguire alcune operazioni. Per un esempio, vedere questo post di StackOverflow.

Un modo alternativo per eseguire la deserializzazione polimorfica

È possibile chiamare Deserialize nel Read metodo :

  • Creare un clone dell'istanza Utf8JsonReader di . Poiché Utf8JsonReader è uno struct, è sufficiente un'istruzione di assegnazione.
  • Usare il clone per leggere i token discriminatori.
  • Chiamare Deserialize usando l'istanza originale Reader dopo aver appreso il tipo necessario. È possibile chiamare Deserialize perché l'istanza originale Reader è ancora posizionata per leggere il token di inizio oggetto.

Uno svantaggio di questo metodo è che non è possibile passare l'istanza di opzioni originale che registra il convertitore in Deserialize. In questo modo si verificherebbe un overflow dello stack, come illustrato in Proprietà obbligatorie. L'esempio seguente illustra un Read metodo che usa questa alternativa:

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

Round trip di supporto per Stack i tipi

Se si deserializza una stringa JSON in un Stack oggetto e quindi si serializza tale oggetto, il contenuto dello stack è in ordine inverso. Questo comportamento si applica ai tipi e alle interfacce seguenti e ai tipi definiti dall'utente che derivano da essi:

Per supportare la serializzazione e la deserializzazione che mantiene l'ordine originale nello stack, è necessario un convertitore personalizzato.

Il codice seguente mostra un convertitore personalizzato che consente il round trip verso e da Stack<T> oggetti:

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

Il codice seguente registra il convertitore:

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

Criteri di denominazione per la deserializzazione delle stringhe enumerazioni

Per impostazione predefinita, il valore predefinito JsonStringEnumConverter può serializzare e deserializzare i valori stringa per le enumerazioni. Funziona senza un criterio di denominazione specificato o con i criteri di CamelCase denominazione. Non supporta altri criteri di denominazione, ad esempio il caso serpente. Per informazioni sul codice del convertitore personalizzato in grado di supportare il round trip verso e dai valori di stringa di enumerazione durante l'uso di un criterio di denominazione dei maiuscole/minuscoli, vedere Problema di GitHub dotnet/runtime #31619. In alternativa, eseguire l'aggiornamento a .NET 7 o versioni successive, che forniscono il supporto predefinito per l'applicazione di criteri di denominazione durante il round trip ai valori di stringa enumerazione.

Usare il convertitore di sistema predefinito

In alcuni scenari, è possibile usare il convertitore di sistema predefinito in un convertitore personalizzato. A tale scopo, ottenere il convertitore di sistema dalla JsonSerializerOptions.Default proprietà , come illustrato nell'esempio seguente:

public class MyCustomConverter : JsonConverter<int>
{
    private readonly static JsonConverter<int> s_defaultConverter = 
        (JsonConverter<int>)JsonSerializerOptions.Default.GetConverter(typeof(int));

    // Custom serialization logic
    public override void Write(
        Utf8JsonWriter writer, int value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }

    // Fall back to default deserialization logic
    public override int Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return s_defaultConverter.Read(ref reader, typeToConvert, options);
    }
}

Gestire i valori Null

Per impostazione predefinita, il serializzatore gestisce i valori Null come segue:

  • Per tipi e Nullable<T> tipi di riferimento:

    • Non passa null ai convertitori personalizzati sulla serializzazione.
    • Non passa JsonTokenType.Null ai convertitori personalizzati sulla deserializzazione.
    • Restituisce un'istanza null in caso di deserializzazione.
    • null Scrive direttamente con il writer sulla serializzazione.
  • Per i tipi valore non nullable:

    • JsonTokenType.Null Passa ai convertitori personalizzati alla deserializzazione. Se non è disponibile alcun convertitore personalizzato, viene generata un'eccezione JsonException dal convertitore interno per il tipo.

Questo comportamento di gestione dei valori Null è principalmente per ottimizzare le prestazioni ignorando una chiamata aggiuntiva al convertitore. Inoltre, evita di forzare i convertitori per i tipi nullable per verificare la presenza null all'inizio dell'override di ogni Read metodo e Write .

Per consentire a un convertitore personalizzato di gestire null per un tipo riferimento o valore, eseguire l'override JsonConverter<T>.HandleNull per restituire true, come illustrato nell'esempio seguente:

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.

Mantenere i riferimenti

Per impostazione predefinita, i dati di riferimento vengono memorizzati nella cache solo per ogni chiamata a Serialize o Deserialize. Per rendere persistenti i riferimenti da una Serialize/Deserialize chiamata a un'altra, eseguire la radice dell'istanza ReferenceResolver nel sito di chiamata di .Serialize/Deserialize Il codice seguente illustra un esempio per questo scenario:

  • Si scrive un convertitore personalizzato per il Company tipo.
  • Non si vuole serializzare manualmente la Supervisor proprietà , ovvero .Employee Si vuole delegare tale valore al serializzatore e si desidera conservare anche i riferimenti già salvati.

Di seguito sono riportate le Employee classi e 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; }
}

Il convertitore è simile al seguente:

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

Classe che deriva da ReferenceResolver archivia i riferimenti in un dizionario:

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

Una classe che deriva da ReferenceHandler contiene un'istanza di MyReferenceResolver e crea una nuova istanza solo quando necessario (in un metodo denominato Reset in questo esempio):

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

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

Quando il codice di esempio chiama il serializzatore, usa un'istanza JsonSerializerOptions in cui la ReferenceHandler proprietà è impostata su un'istanza di MyReferenceHandler. Quando si segue questo modello, assicurarsi di reimpostare il dizionario al termine della ReferenceResolver serializzazione, per impedirne la crescita per sempre.

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'esempio precedente esegue solo la serializzazione, ma è possibile adottare un approccio simile per la deserializzazione.

Altri esempi di convertitori personalizzati

L'articolo Eseguire la migrazione da Newtonsoft.Json a System.Text.Json contiene esempi aggiuntivi di convertitori personalizzati.

La cartella unit test nel System.Text.Json.Serialization codice sorgente include altri esempi di convertitori personalizzati, ad esempio:

Se è necessario creare un convertitore che modifica il comportamento di un convertitore predefinito esistente, è possibile ottenere il codice sorgente del convertitore esistente da usare come punto di partenza per la personalizzazione.

Risorse aggiuntive