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:
- Deserializzare i tipi dedotti alle proprietà dell'oggetto.
- Supportare la deserializzazione polimorfica.
- Supporto round trip per
Stack
i tipi. - Usare il convertitore di sistema predefinito.
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:
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 tipoT
. 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 restituiscatrue
. - Eseguire l'override del
Write
metodo per serializzare l'oggetto in ingresso di tipoT
. Usare l'oggetto Utf8JsonWriter passato al metodo per scrivere il codice JSON. - Eseguire l'override del
CanConvert
metodo solo se necessario. L'implementazione predefinita restituiscetrue
quando il tipo da convertire è di tipoT
. Pertanto, i convertitori che supportano solo il tipoT
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 restituiretrue
quando il tipo da convertire è uno che il convertitore può gestire. Ad esempio, se il convertitore è perList<T>
, potrebbe gestireList<int>
solo ,List<string>
eList<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
(WeekdaysEnum
ad 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.
- Deserializzare i tipi dedotti alle proprietà dell'oggetto.
- Supporto round trip per
Stack
i tipi. - Usare il convertitore di sistema predefinito.
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 Boolean
e 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 ulong
BigInteger
. 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
efalse
aBoolean
- 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 originaleReader
dopo aver appreso il tipo necessario. È possibile chiamareDeserialize
perché l'istanza originaleReader
è 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.
- Non passa
Per i tipi valore non nullable:
JsonTokenType.Null
Passa ai convertitori personalizzati alla deserializzazione. Se non è disponibile alcun convertitore personalizzato, viene generata un'eccezioneJsonException
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:
- Convertitore Int32 che converte null in 0 in deserializzare
- Convertitore Int32 che consente valori stringa e numerici sulla deserializzazione
- Convertitore di enumerazioni
- Elencare<il convertitore T> che accetta dati esterni
- Convertitore Long[] che funziona con un elenco delimitato da virgole di numeri
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.