Jak psát vlastní převaděče pro serializaci JSON (zařazování) v .NET
Tento článek ukazuje, jak vytvořit vlastní převaděče pro třídy serializace JSON, které jsou poskytovány System.Text.Json v oboru názvů. Úvod do System.Text.Json
tématu Jak serializovat a deserializovat JSON v .NET.
Převaděč je třída, která převádí objekt nebo hodnotu do a z JSON. Obor System.Text.Json
názvů obsahuje integrované převaděče pro většinu primitivních typů, které se mapují na primitiva JavaScriptu. Můžete napsat vlastní převaděče, které přepíší výchozí chování integrovaného převaděče. Příklad:
- Můžete chtít
DateTime
, aby hodnoty byly reprezentovány formátem mm/dd/rrrr. Ve výchozím nastavení se podporuje ISO 8601-1:2019, včetně profilu RFC 3339. Další informace naleznete v tématu DateTime a DateTimeOffset podpora v System.Text.Json. - Můžete chtít serializovat POCO jako řetězec JSON, například s typem
PhoneNumber
.
Můžete také psát vlastní převaděče pro přizpůsobení nebo rozšíření System.Text.Json
o nové funkce. Následující scénáře jsou popsané dále v tomto článku:
Jazyk Visual Basic se nedá použít k psaní vlastních převaděčů, ale může volat převaděče implementované v knihovnách jazyka C#. Další informace naleznete v tématu Podpora jazyka Visual Basic.
Vzory vlastních převaděčů
Existují dva vzory pro vytvoření vlastního převaděče: základní vzor a vzor továrny. Vzor továrny je určen pro převaděče, které zpracovávají typ Enum
nebo otevírají obecné typy. Základní vzor je určený pro obecné a uzavřené obecné typy. Například převaděče pro následující typy vyžadují vzor továrny:
Mezi příklady typů, které je možné zpracovat základním vzorem, patří:
Základní vzor vytvoří třídu, která dokáže zpracovat jeden typ. Vzor továrny vytvoří třídu, která určuje, v době běhu, který konkrétní typ je vyžadován a dynamicky vytvoří příslušný převaděč.
Ukázkový základní převaděč
Následující ukázka je převaděč, který přepíše výchozí serializaci pro existující datový typ. Převaděč používá pro DateTimeOffset
vlastnosti formát mm/dd/rrrr.
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));
}
}
Převaděč vzorů ukázkové továrny
Následující kód ukazuje vlastní převaděč, který funguje s Dictionary<Enum,TValue>
. Kód se řídí vzorem továrny, protože první parametr obecného typu je Enum
a druhý je otevřený. Metoda CanConvert
vrátí true
pouze pro Dictionary
dva obecné parametry, z nichž první je Enum
typ. Vnitřní převaděč získá existující převaděč pro zpracování toho, který typ je poskytován za běhu pro 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();
}
}
}
}
Postup základního vzoru
Následující kroky vysvětlují, jak vytvořit převaděč pomocí základního vzoru:
- Vytvořte třídu, která je odvozena od JsonConverter<T> where
T
je typ, který má být serializován a deserializován. - Přepsat metodu
Read
deserializovat příchozí JSON a převést ji na typT
. Utf8JsonReader Použijte metodu předanou ke čtení JSON. Nemusíte se starat o zpracování částečných dat, protože serializátor předává všechna data pro aktuální obor JSON. Proto není nutné volat Skip nebo TrySkip ověřit, že Read se vrátítrue
. - Přepsat metodu
Write
serializovat příchozí objekt typuT
. Utf8JsonWriter Použijte metodu předanou k zápisu JSON. - Přepsat metodu
CanConvert
pouze v případě potřeby. Výchozí implementace vrátítrue
, když typ převodu je typuT
. Převaděče, které podporují pouze typT
, proto tuto metodu nemusí přepsat. Příklad převaděče, který potřebuje tuto metodu přepsat, naleznete v části polymorfní deserializace dále v tomto článku.
Můžete odkazovat na předdefinované převaděče zdrojový kód jako referenční implementace pro psaní vlastních převaděčů.
Postup použití vzoru továrny
Následující kroky vysvětlují, jak vytvořit převaděč podle vzoru továrny:
- Vytvoření třídy, která je odvozena od JsonConverterFactory.
- Přepište metodu
CanConvert
, která se má vrátittrue
, když typ převodu je ten, který převaděč dokáže zpracovat. Například pokud je převaděč proList<T>
, může zpracovatList<int>
pouze ,List<string>
aList<DateTime>
. - Přepište metodu
CreateConverter
pro vrácení instance třídy převaděče, která bude zpracovávat typ-to-convert, který je k dispozici za běhu. - Vytvořte třídu převaděče, kterou
CreateConverter
metoda vytvoří instanci.
Vzor továrny se vyžaduje pro otevřené obecné typy, protože kód pro převod objektu na řetězec a z řetězce není stejný pro všechny typy. Převaděč pro otevřený obecný typ (List<T>
například) musí vytvořit převaděč pro uzavřený obecný typ (List<DateTime>
například) na pozadí. Kód musí být zapsán pro zpracování každého uzavřeného obecného typu, který převaděč dokáže zpracovat.
Typ Enum
je podobný otevřenému obecnému typu: převaděč pro Enum
vytvoření převaděče pro konkrétní Enum
(WeekdaysEnum
například) na pozadí.
Použití Utf8JsonReader
v Read
metodě
Pokud převaděč převádí objekt JSON, umístí se Utf8JsonReader
na počáteční token objektu při Read
zahájení metody. Pak musíte přečíst všechny tokeny v daném objektu a ukončit metodu se čtečkou umístěnou na odpovídajícím koncovém tokenu objektu. Pokud před dosažením odpovídajícího koncového tokenu přečtete za konec objektu nebo pokud se zastavíte, zobrazí se JsonException
výjimka, která značí, že:
Převaděč Converter 'ConverterName' číst příliš mnoho nebo nestačí.
Příklad najdete v ukázkovém převaděči vzorů předchozí továrny. Metoda Read
začíná ověřením, že je čtenář umístěn na počáteční token objektu. Přečte, dokud nenajde, že je umístěn na dalším koncovém tokenu objektu. Zastaví se na dalším koncovém tokenu objektu, protože neexistují žádné počáteční tokeny objektu, které by označovaly objekt uvnitř objektu. Stejné pravidlo o počátečním a koncovém tokenu platí, pokud převádíte pole. Příklad najdete v ukázkovém Stack<T>
převaděči dále v tomto článku.
Zpracování chyb
Serializátor poskytuje speciální zpracování pro typy JsonException výjimek a NotSupportedException.
JsonException
Pokud vyvoláte JsonException
zprávu bez zprávy, serializátor vytvoří zprávu, která obsahuje cestu k části JSON, která způsobila chybu. Příkaz throw new JsonException()
například vytvoří chybovou zprávu jako v následujícím příkladu:
Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.
Pokud zadáte zprávu (například throw new JsonException("Error occurred")
), serializátor stále nastaví Path, LineNumbera BytePositionInLine vlastnosti.
Notsupportedexception
Pokud vyvoláte zprávu NotSupportedException
, vždy získáte informace o cestě ve zprávě. Pokud zadáte zprávu, připojí se k ní informace o cestě. Příkaz throw new NotSupportedException("Error occurred.")
například vytvoří chybovou zprávu jako v následujícím příkladu:
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
Kdy vyvolat typ výjimky
Pokud datová část JSON obsahuje tokeny, které nejsou platné pro deserializovaný typ, vyvolte výjimku JsonException
.
Pokud chcete zakázat určité typy, vyhoďte .NotSupportedException
Tato výjimka je to, co serializátor automaticky vyvolá pro typy, které nejsou podporovány. Například System.Type
není podporován z bezpečnostních důvodů, takže pokus o deserializaci má za NotSupportedException
následek .
Podle potřeby můžete vyvolat další výjimky, ale neobsahují informace o cestě JSON automaticky.
Registrace vlastního převaděče
Zaregistrujte vlastní převaděč, aby ho Serialize
používaly a Deserialize
používaly. Zvolte jeden z následujících přístupů:
- Přidejte instanci třídy převaděče JsonSerializerOptions.Converters do kolekce.
- Použijte atribut [JsonConverter] na vlastnosti, které vyžadují vlastní převaděč.
- Použijte atribut [JsonConverter] na třídu nebo strukturu, která představuje vlastní typ hodnoty.
Ukázka registrace – Kolekce převaděčů
Tady je příklad, který nastaví DateTimeOffsetJsonConverter jako výchozí pro vlastnosti typu DateTimeOffset:
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true,
Converters =
{
new DateTimeOffsetJsonConverter()
}
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);
Předpokládejme, že serializujete instanci následujícího typu:
public class WeatherForecast
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Tady je příklad výstupu JSON, který ukazuje použití vlastního převaděče:
{
"Date": "08/01/2019",
"TemperatureCelsius": 25,
"Summary": "Hot"
}
Následující kód používá stejný přístup k deserializaci pomocí vlastního DateTimeOffset
převaděče:
var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions)!;
Ukázka registrace – [JsonConverter] u vlastnosti
Následující kód vybere vlastní převaděč vlastnosti Date
:
public class WeatherForecastWithConverterAttribute
{
[JsonConverter(typeof(DateTimeOffsetJsonConverter))]
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Kód k serializaci WeatherForecastWithConverterAttribute
nevyžaduje použití JsonSerializeOptions.Converters
:
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);
Kód pro deserializaci také nevyžaduje použití Converters
:
weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithConverterAttribute>(jsonString)!;
Ukázka registrace – [JsonConverter] u typu
Tady je kód, který vytvoří strukturu a použije [JsonConverter]
na ni atribut:
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);
}
}
}
Tady je vlastní převaděč pro předchozí strukturu:
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());
}
}
Atribut [JsonConverter]
ve struktuře registruje vlastní převaděč jako výchozí pro vlastnosti typu Temperature
. Převaděč se automaticky používá u TemperatureCelsius
vlastnosti následujícího typu při serializaci nebo deserializaci:
public class WeatherForecastWithTemperatureStruct
{
public DateTimeOffset Date { get; set; }
public Temperature TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Priorita registrace převaděče
Během serializace nebo deserializace je převaděč vybrán pro každý prvek JSON v následujícím pořadí, uvedený od nejvyšší priority po nejnižší:
[JsonConverter]
použitý u vlastnosti.- Převaděč přidaný do
Converters
kolekce. [JsonConverter]
použitý pro vlastní typ hodnoty nebo POCO.
Pokud je v Converters
kolekci registrováno více vlastních převaděčů pro určitý typ, použije se první převaděč, který se vrátí true
pro CanConvert
daný typ.
Integrovaný převaděč je zvolen pouze v případě, že není registrován žádný použitelný vlastní převaděč.
Ukázky převaděčů pro běžné scénáře
Následující části obsahují ukázky převaděčů, které řeší některé běžné scénáře, které integrované funkce nezpracují.
Ukázkový DataTable převaděč naleznete v tématu Podporované typy kolekcí.
Deserializace odvozených typů na vlastnosti objektu
Při deserializaci na vlastnost typu object
, JsonElement
objekt je vytvořen. Důvodem je, že deserializátor neví, jaký typ CLR se má vytvořit, a nepokouší se odhadnout. Pokud má například vlastnost JSON hodnotu true, deserializer neodpočítá, že hodnota je a Boolean
pokud prvek má hodnotu 01/01/2019, deserializátor neodpočítá, že se jedná o DateTime
.
Odvození typu může být nepřesné. Pokud deserializátor parsuje číslo JSON, které nemá jako desetinnou čárku long
, může vést k problémům mimo rozsah, pokud byla hodnota původně serializována jako ulong
nebo BigInteger
. Analýza čísla, které má desetinnou čárku jako desetinnou čárku double
, může přijít o přesnost, pokud bylo číslo původně serializováno jako decimal
.
V případě scénářů, které vyžadují odvození typu, následující kód zobrazí vlastní převaděč vlastností object
. Kód převede:
true
afalse
doBoolean
- Čísla bez desetinné čárky
long
- Čísla s desetinným číslem na
double
- Data do
DateTime
- Řetězce pro
string
- Všechno ostatní na
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"
//}
Příklad ukazuje kód převaděče a WeatherForecast
třídu s vlastnostmi object
. Metoda Main
deserializuje řetězec JSON do WeatherForecast
instance, nejprve bez použití převaděče a pak pomocí převaděče. Výstup konzoly ukazuje, že bez převaděče je typ běhu vlastnosti Date
JsonElement
; s převaděčem, typ běhu je DateTime
.
Složka testů jednotek v System.Text.Json.Serialization
oboru názvů obsahuje více příkladů vlastních převaděčů, které zpracovávají deserializaci na object
vlastnosti.
Podpora polymorfní deserializace
.NET 7 poskytuje podporu polymorfní serializace i deserializace. V předchozích verzích .NET však byla omezena podpora polymorfní serializace a nebyla podporována deserializace. Pokud používáte .NET 6 nebo starší verzi, deserializace vyžaduje vlastní převaděč.
Předpokládejme například, že máte Person
abstraktní základní třídu s odvozenými třídami a Customer
odvozenými třídamiEmployee
. Polymorfní deserializace znamená, že v době návrhu můžete zadat Person
jako cíl deserializace a Customer
Employee
objekty ve formátu JSON jsou správně deserializovány za běhu. Během deserializace musíte najít vodítka, která identifikují požadovaný typ ve formátu JSON. Různé druhy nápovědy, které jsou k dispozici, se liší v každém scénáři. Například může být k dispozici diskriminující vlastnost nebo se budete muset spolehnout na přítomnost nebo nepřítomnost konkrétní vlastnosti. Aktuální verze System.Text.Json
neposkytuje atributy, které určují, jak zpracovávat polymorfní deserializační scénáře, takže jsou vyžadovány vlastní převaděče.
Následující kód ukazuje základní třídu, dvě odvozené třídy a vlastní převaděč pro ně. Převaděč používá diskriminující vlastnost k polymorfní deserializaci. Typ diskriminátor není v definicích třídy, ale je vytvořen během serializace a je čtena během deserializace.
Důležité
Ukázkový kód vyžaduje, aby páry názvu a hodnoty objektu JSON zůstaly v pořádku, což není standardní požadavek 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();
}
}
}
Následující kód zaregistruje převaděč:
var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new PersonConverterWithTypeDiscriminator());
Převaděč může deserializovat JSON, který byl vytvořen pomocí stejného převaděče pro serializaci, například:
[
{
"TypeDiscriminator": 1,
"CreditLimit": 10000,
"Name": "John"
},
{
"TypeDiscriminator": 2,
"OfficeNumber": "555-1234",
"Name": "Nancy"
}
]
Kód převaděče v předchozím příkladu čte a zapisuje každou vlastnost ručně. Alternativou je volání Deserialize
nebo Serialize
provedení některé práce. Příklad najdete v tomto příspěvku StackOverflow.
Alternativní způsob polymorfní deserializace
Metodu Read
můžete volatDeserialize
:
- Vytvořte klon
Utf8JsonReader
instance. Vzhledem k tomuUtf8JsonReader
, že se jedná o strukturu, stačí příkaz přiřazení. - Pomocí klonu můžete číst diskriminující tokeny.
- Jakmile znáte typ, který potřebujete, zavolejte
Deserialize
pomocí původníReader
instance. Můžete volatDeserialize
, protože původníReader
instance je stále umístěna ke čtení počátečního tokenu objektu.
Nevýhodou této metody je, že nemůžete předat původní instanci možností, která registruje převaděč do Deserialize
. To by způsobilo přetečení zásobníku, jak je vysvětleno v povinných vlastnostech. Následující příklad ukazuje metodu Read
, která používá tuto alternativu:
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;
}
Podpora odezvy pro Stack
typy
Pokud deserializujete řetězec JSON do objektu Stack
a potom serializujete tento objekt, obsah zásobníku je v obráceném pořadí. Toto chování platí pro následující typy a rozhraní a uživatelem definované typy, které jsou z nich odvozeny:
K podpoře serializace a deserializace, která zachovává původní pořadí v zásobníku, je vyžadován vlastní převaděč.
Následující kód ukazuje vlastní převaděč, který umožňuje přecházení do a z Stack<T>
objektů:
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();
}
}
}
Následující kód zaregistruje převaděč:
var options = new JsonSerializerOptions
{
Converters = { new JsonConverterFactoryForStackOfT() },
};
Zásady pojmenování pro deserializaci řetězců výčtu
Ve výchozím nastavení může předdefinovaný JsonStringEnumConverter serializovat a deserializovat řetězcové hodnoty pro výčty. Funguje bez zadané zásady pojmenování nebo se CamelCase zásadami pojmenování. Nepodporuje jiné zásady pojmenování, jako je případ hada. Informace o kódu vlastního převaděče, který může podporovat zaokrouhlování na řetězcové hodnoty výčtu a hodnoty z nich při používání zásad pojmenování hadů, najdete v tématu problém GitHubu dotnet/runtime #31619. Alternativně upgradujte na .NET 7 nebo novější verze, které poskytují integrovanou podporu pro použití zásad pojmenování při zaokrouhlování na řetězcové hodnoty výčtu a z nich.
Použití výchozího převaděče systému
V některých scénářích můžete chtít použít výchozí převaděč systému ve vlastním převaděči. Chcete-li to provést, získejte systémový převaděč z JsonSerializerOptions.Default vlastnosti, jak je znázorněno v následujícím příkladu:
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);
}
}
Zpracování hodnot null
Serializátor ve výchozím nastavení zpracovává hodnoty null následujícím způsobem:
Pro referenční typy a Nullable<T> typy:
- Při serializaci nepředává
null
vlastní převaděče. - Při deserializaci se nepředává
JsonTokenType.Null
do vlastních převaděčů. - Vrátí
null
instanci při deserializaci. - Zapisuje
null
přímo se zapisovačem při serializaci.
- Při serializaci nepředává
Pro typy hodnot bez hodnoty null:
JsonTokenType.Null
Předává se vlastním převaděčům při deserializaci. (Pokud není k dispozici žádný vlastní převaděč,JsonException
vyvolá se výjimka interním převaděčem pro typ.)
Toto chování zpracování null je primárně pro optimalizaci výkonu přeskočením nadbytečného volání převaděče. Kromě toho zabraňuje vynucení převaděčů pro typy s možnou hodnotou null kontrolovat null
na začátku každého Read
přepsání a Write
metody.
Chcete-li povolit vlastní převaděč pro zpracování null
odkazu nebo typu hodnoty, přepsání JsonConverter<T>.HandleNull vrátit true
, jak je znázorněno v následujícím příkladu:
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.
Zachování odkazů
Ve výchozím nastavení se referenční data ukládají pouze do mezipaměti pro každé volání nebo SerializeDeserialize. Chcete-li zachovat odkazy z jednoho Serialize
/Deserialize
volání do druhého, kořen instance ReferenceResolver v lokalitě Serialize
/Deserialize
volání . Následující kód ukazuje příklad pro tento scénář:
- Napíšete vlastní převaděč pro
Company
typ. - Nechcete vlastnost serializovat
Supervisor
ručně, což je .Employee
Chcete tuto možnost delegovat na serializátor a chcete zachovat také odkazy, které jste již uložili.
Tady jsou třídyEmployee
: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; }
}
Převaděč vypadá takto:
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();
}
}
Třída odvozená od ReferenceResolver úložišť odkazů ve slovníku:
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;
}
}
Třída, která je odvozena z ReferenceHandler blokování instance MyReferenceResolver
a vytváří novou instanci pouze v případě potřeby (v metodě pojmenované Reset
v tomto příkladu):
class MyReferenceHandler : ReferenceHandler
{
public MyReferenceHandler() => Reset();
private ReferenceResolver? _rootedResolver;
public override ReferenceResolver CreateResolver() => _rootedResolver!;
public void Reset() => _rootedResolver = new MyReferenceResolver();
}
Když vzorový kód volá serializátor, používá JsonSerializerOptions instanci, ve které ReferenceHandler je vlastnost nastavena na instanci MyReferenceHandler
. Když budete postupovat podle tohoto vzoru, nezapomeňte po dokončení serializace obnovit ReferenceResolver
slovník, aby se nezvětšil navždy.
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();
Předchozí příklad pouze serializace, ale podobný přístup lze přijmout pro deserializaci.
Další ukázky vlastních převaděčů
Migrace z Newtonsoft.JsonSystem.Text.Json článku obsahuje další ukázky vlastních převaděčů.
Složka testů jednotek ve zdrojovém System.Text.Json.Serialization
kódu obsahuje další vlastní ukázky převaděčů, například:
- Převaděč Int32, který při deserializaci převede hodnotu null na 0
- Převaděč Int32, který umožňuje deserializovat řetězcové i číselné hodnoty
- Převaděč výčtů
- Seznam<převaděče T> , který přijímá externí data
- Převaděč Long[] , který funguje se seznamem čísel oddělených čárkami
Pokud potřebujete vytvořit převaděč, který upravuje chování existujícího integrovaného převaděče, můžete získat zdrojový kód existujícího převaděče , který bude sloužit jako výchozí bod pro přizpůsobení.