Condividi tramite


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

Questo articolo illustra come creare convertitori personalizzati per le classi di serializzazione JSON fornite nello spazio dei nomi System.Text.Json. 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 spazio dei nomi System.Text.Json include convertitori predefiniti per la maggior parte dei tipi primitivi che eseguono il mapping ai tipi primitivi 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 trattati più avanti in questo articolo:

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

Criteri di convertitore personalizzati

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

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

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

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

Esempio di convertitore di base

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 le proprietà DateTimeOffset.

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

Esempio di convertitore di criterio factory

Il codice seguente illustra un convertitore personalizzato che funziona con Dictionary<Enum,TValue>. Il codice segue il criterio factory perché il primo parametro di tipo generico è Enum e il secondo è aperto. Il metodo CanConvert restituisce true solo per un Dictionary con due parametri generici, il primo dei quali è di tipo Enum. 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 criterio di base

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

  • Creare una classe che deriva da JsonConverter<T> dove T è il tipo da serializzare e deserializzare.
  • Eseguire l'override del metodo Read per deserializzare il codice JSON in ingresso e convertirlo nel 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 utilizzare Skip o TrySkip per convalidare che Read restituisca true.
  • Eseguire l'override del metodo Write 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 metodo CanConvert solo se necessario. L'implementazione predefinita restituisce true quando il tipo da convertire è del 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 sulla 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 criterio factory

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

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

Il criterio factory è necessario per i tipi generici 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 è in grado di gestire.

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

Uso di Utf8JsonReader nel metodo Read

Se il convertitore converte un oggetto JSON, l'oggetto Utf8JsonReader verrà posizionato sul token dell'oggetto iniziale all'avvio del metodo Read. È quindi necessario leggere tutti i token presenti 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 criterio factory precedente. Il metodo Read 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 iniziale e al token finale si applica se si sta convertendo una matrice. Per un esempio, vedere il codice di esempio più avanti in questo articolo.

Gestione errori

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

JsonException

Se si genera un oggetto JsonException 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 proprietà Path, LineNumber e BytePositionInLine.

NotSupportedException

Se si genera una NotSupportedException, si ottengono sempre le informazioni sul percorso nel messaggio. Se si specifica un messaggio, ad esso vengono aggiunte le informazioni sul percorso. 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 quale tipo di eccezione

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

Quando si desidera impedire determinati tipi, generare una 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 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 metodi Serialize e Deserialize. Adottare uno degli approcci seguenti:

  • Aggiungere un'istanza della classe convertitore alla collezione JsonSerializerOptions.Converters.
  • 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 - Raccolta di convertitori

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

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true
};
serializeOptions.Converters.Add(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] su una proprietà

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

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

Anche il codice da deserializzare non richiede l'uso di Converters:

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

Esempio di registrazione - [JsonConverter] su 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 la 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 proprietà TemperatureCelsius del tipo seguente durante la serializzazione o la deserializzazione:

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

Precedenza nella registrazione dei convertitori

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

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

Se nella raccolta Converters vengono registrati 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 convertitori che riguardano alcuni scenari comuni non gestiti da funzionalità predefinite.

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

Deserializzare i tipi dedotti alle proprietà oggetto

Quando si deserializza in una proprietà di tipo object, viene creato un oggetto JsonElement. 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 sia un Boolean e se un elemento ha "01/01/2019", il deserializzatore non deduce che sia un oggetto DateTime.

L'inferenza del tipo può essere inaccurata. 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 ulong o BigInteger. L'analisi di un numero con un separatore decimale come double può risultare inaccurato 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 le proprietà object. Il codice converte:

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

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

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 Personclasse base astratta con classi derivate Employee e Customer. La deserializzazione polimorfica implica che in fase di progettazione è possibile specificare Person come destinazione di deserializzazione e gli oggetti Customer e Employee nel codice JSON vengono deserializzati correttamente in fase di esecuzione. Durante la deserializzazione, è necessario trovare indizi che identifichino il tipo richiesto nel codice JSON. I tipi di indizi disponibili variano in base agli scenari. Ad esempio, una proprietà Discriminator 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 radice, due classi derivate e un convertitore personalizzato per tali classi. Il convertitore usa una proprietà Discriminator per eseguire la deserializzazione polimorfica. Il Discriminator 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 è in grado di deserializzare JSON, che è stato creato usando lo stesso convertitore per la serializzazione, 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 nell'utilizzare Deserialize o Serialize per eseguire alcune operazioni. Per altre informazioni, vedere questo post su StackOverflow.

Un modo alternativo per eseguire la deserializzazione polimorfica

È possibile utilizzare Deserialize nel metodo Read:

  • Creare un clone dell'istanza di Utf8JsonReader. Poiché Utf8JsonReader è uno struct, è sufficiente un'istruzione di assegnazione.
  • Usare il clone per leggere i token Discriminator.
  • Utilizzare Deserialize usando l'istanza originale Reader dopo aver appreso il tipo necessario. È possibile utilizzare 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. Nell'esempio seguente viene illustrato un metodo Readche utilizza 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;
}

Supportare round trip per i tipi Stack

Se si deserializza una stringa JSON in un oggetto Stack 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 ne derivano:

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 oggetti Stack<T>:

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();
options.Converters.Add(new JsonConverterFactoryForStackOfT());

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 proprietà JsonSerializerOptions.Default, 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 i tipi di riferimento e i tipi Nullable<T>:

    • Non passa null ai convertitori personalizzati durante la serializzazione.
    • Non passa JsonTokenType.Null ai convertitori personalizzati durante la deserializzazione.
    • Restituisce un'istanza null durante la deserializzazione.
    • Scrive direttamente null con il writer durante la serializzazione.
  • Per i tipi valore che non ammettono i valori null:

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

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

Per consentire a un convertitore personalizzato di gestire null per un tipo di riferimento o di valore, eseguire l'override di 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 chiamata Serialize/Deserialize a un'altra, eseguire la radice dell'istanza ReferenceResolver nel sito di chiamata di Serialize/Deserialize. Il codice seguente mostra un esempio per questo scenario:

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

Ecco le classi Employee 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();
    }
}

Una 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 di JsonSerializerOptions in cui la proprietà ReferenceHandler è impostata su un'istanza di MyReferenceHandler. Quando si segue questo modello, assicurarsi di reimpostare il dizionario ReferenceResolver al termine della serializzazione, per evitare che cresca all'infinito.

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 codice sorgente System.Text.Json.Serialization 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