Egyéni konverterek írása JSON-szerializáláshoz (rendezéshez) a .NET-ben
Ez a cikk bemutatja, hogyan hozhat létre egyéni konvertereket a névtérben megadott JSON szerializálási System.Text.Json osztályokhoz. A bevezetést a System.Text.Json
JSON szerializálása és deszerializálása a .NET-ben című témakörben talál.
A konverter olyan osztály, amely egy objektumot vagy értéket JSON-ra és JSON-ra konvertál. A System.Text.Json
névtér beépített konverterekkel rendelkezik a JavaScript-primitívekhez kapcsolódó legtöbb primitív típushoz. Egyéni konvertereket írhat egy beépített konverter alapértelmezett viselkedésének felülbírálásához. Példa:
- Előfordulhat, hogy az értékeket mm/dd/yyy formátumban szeretné
DateTime
ábrázolni. Alapértelmezés szerint az ISO 8601-1:2019 támogatott, beleértve az RFC 3339-profilt is. További információ: DateTime és DateTimeOffset támogatás a System.Text.Json. - Előfordulhat, hogy JSON-sztringként szeretne szerializálni egy POCO-t, például egy
PhoneNumber
típussal.
Egyéni konvertereket is írhat az új funkciók testreszabásához vagy kibővítéséhez System.Text.Json
. A cikk későbbi részében a következő forgatókönyveket ismerteti:
- A kikövetkeztetett típusok deszerializálása objektumtulajdonságokra.
- Támogatja a polimorf deszerializálást.
- Típusok esetén támogatja az oda-vissza utazást
Stack
. - Használja az alapértelmezett rendszerkonvertert.
- A kikövetkeztetett típusok deszerializálása objektumtulajdonságokra.
- Támogatja a polimorf deszerializálást.
- Típusok esetén támogatja az oda-vissza utazást
Stack
. - Elnevezési szabályzatok a szám sztring deszerializálásához.
A Visual Basic nem használható egyéni konverterek írására, de C#-kódtárakban implementált konvertereket is meghívhat. További információ: Visual Basic-támogatás.
Egyéni konverterminták
Az egyéni konverter létrehozásához két minta létezik: az alapszintű és a gyári minta. A gyári minta olyan konverterekhez készült, amelyek típust Enum
vagy nyitott általános generikusokat kezelnek. Az alapminta nem általános és zárt általános típusok esetében használható. A következő típusok konverterei például a gyári mintát igénylik:
Néhány példa az alapszintű mintával kezelhető típusokra:
Az alapszintű minta egy olyan osztályt hoz létre, amely képes kezelni egy típust. A gyári minta létrehoz egy osztályt, amely futásidőben meghatározza, hogy melyik típusra van szükség, és dinamikusan létrehozza a megfelelő konvertert.
Minta alapkonverter
Az alábbi minta egy konverter, amely felülbírálja egy meglévő adattípus alapértelmezett szerializálását. A konverter mm/dd/yyy formátumot használ a tulajdonságokhoz 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));
}
}
Minta-előállító mintakonverter
Az alábbi kód egy egyéni konvertert mutat be, amely a következővel Dictionary<Enum,TValue>
működik: A kód a gyári mintát követi, mert az első általános típusparaméter, Enum
a második pedig nyitva van. A CanConvert
metódus csak két általános paramétert ad true
Dictionary
vissza, amelyek közül az első típus Enum
. A belső konverter egy meglévő konvertert kap, amely a futtatáskor TValue
megadott típust kezeli.
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();
}
}
}
}
Az alapszintű minta követésének lépései
Az alábbi lépések az alapszintű mintát követve ismertetik, hogyan hozhat létre konvertert:
- Hozzon létre egy osztályt, amely abból a típusból JsonConverter<T>
T
származik, amelyet szerializálni és deszerializálni kell. - Felülbírálja a
Read
bejövő JSON deszerializálására és típussáT
alakítására vonatkozó metódust. A JSON olvasásához használja a Utf8JsonReader metódusnak átadott adatokat. Nem kell aggódnia a részleges adatok kezelése miatt, mivel a szerializáló az aktuális JSON-hatókör összes adatát átadja. Ezért nem szükséges meghívni Skip vagy TrySkip ellenőrizni a Read visszatérésttrue
. - Felülbírálja a metódust
Write
a bejövő típusú objektum szerializálásáhozT
. A JSON írásához használja a Utf8JsonWriter metódusnak átadott parancsot. - A metódust
CanConvert
csak szükség esetén bírálja felül. Az alapértelmezett implementáció akkor tér visszatrue
, ha az átalakítandó típus típusT
. Ezért a csak típustT
támogató konvertereknek nem kell felülbírálniuk ezt a módszert. A metódus felülbírálásához szükséges konverterre a cikk későbbi, polimorf deszerializálási szakaszában talál példát.
Az egyéni konverterek írásához referencia-implementációként hivatkozhat a beépített konverterek forráskódjaira .
A gyári minta követésének lépései
Az alábbi lépések bemutatják, hogyan hozhat létre konvertert a gyári minta követésével:
- Hozzon létre egy osztályt, amely a forrásból JsonConverterFactoryszármazik.
- Felülbírálja a
CanConvert
visszatérésitrue
metódust, ha az átalakítandó típus az, amelyet a konverter képes kezelni. Ha például a konverter a következőhöz tartozik, akkor előfordulhat, hogy csak a következőtList<T>
kezeliList<int>
List<string>
List<DateTime>
: - Felülbírálja a
CreateConverter
metódust egy konverterosztály egy példányának visszaadásához, amely kezeli a futtatáskor megadott konvertálási típust. - Hozza létre a metódus által példányosított konverterosztályt
CreateConverter
.
A gyári minta a nyitott általánosakhoz szükséges, mert az objektum sztringgé alakítására szolgáló kód nem minden típus esetében ugyanaz. Egy nyitott általános típus konverterének (List<T>
például) létre kell hoznia egy konvertert egy zárt általános típushoz (List<DateTime>
például) a színfalak mögött. A kódot úgy kell írni, hogy a konverter képes legyen kezelni az összes olyan zárt generikus típust, amelyet a konverter képes kezelni.
A Enum
típus hasonló egy nyílt általános típushoz: a konverternek Enum
létre kell hoznia egy konvertert egy adott Enum
(WeekdaysEnum
például) színfalak mögötti konverterhez.
A metódus használata Utf8JsonReader
Read
Ha a konverter JSON-objektumot konvertál, a metódus indításakor Read
a Utf8JsonReader
rendszer a kezdő objektum jogkivonatán lesz elhelyezve. Ezután át kell olvasnia az objektum összes jogkivonatát, és ki kell lépnie a metódusból, és az olvasót a megfelelő végponti jogkivonaton kell elhelyeznie. Ha az objektum végénél tovább olvas, vagy a megfelelő záró jogkivonat elérése előtt leáll, kivétel jelenik meg JsonException
, amely a következőket jelzi:
A "ConverterName" konverter túl sokat olvas vagy nem elég.
Példaként tekintse meg az előző gyári mintakonvertert. A Read
metódus első lépéseként ellenőrizze, hogy az olvasó egy kezdőobjektum-jogkivonaton van-e elhelyezve. Addig olvas, amíg meg nem találja, hogy a következő objektumjogkivonaton van elhelyezve. A következő végobjektum-jogkivonat leáll, mert nincsenek olyan beavatkozó kezdőobjektum-jogkivonatok, amelyek egy objektumot jeleznének az objektumon belül. A kezdő jogkivonatra és a záró jogkivonatra ugyanaz a szabály érvényes, ha tömböt konvertál. Példaként tekintse meg a Stack<T>
cikk későbbi részében található mintakonvertert.
Hibakezelés
A szerializáló speciális kezelést biztosít a kivételtípusokhoz JsonException és NotSupportedExceptiona .
JsonException
Ha üzenet nélkül küld el egy JsonException
üzenetet, a szerializáló létrehoz egy üzenetet, amely tartalmazza a JSON azon részének elérési útját, amely a hibát okozta. Az utasítás throw new JsonException()
például a következő példához hasonló hibaüzenetet jelenít meg:
Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.
Ha mégis megad egy üzenetet (példáulthrow new JsonException("Error occurred")
), a szerializáló továbbra is beállítja a , LineNumberés BytePositionInLine a Pathtulajdonságokat.
NotSupportedException
Ha dob egy NotSupportedException
, mindig megkapja az elérési út adatait az üzenetben. Ha üzenetet ad meg, az elérési út adatai hozzá lesznek fűzve. Az utasítás throw new NotSupportedException("Error occurred.")
például a következő példához hasonló hibaüzenetet jelenít meg:
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
Mikor kell a kivételtípust eldobni?
Ha a JSON hasznos adat olyan jogkivonatokat tartalmaz, amelyek nem érvényesek a deszerializált típusra, dobjon egy JsonException
.
Ha le szeretné tiltani bizonyos típusokat, dobjon egy NotSupportedException
. Ez a kivétel az, amit a szerializáló automatikusan a nem támogatott típusok esetében dob. Biztonsági okokból például System.Type
nem támogatott, ezért a deszerializálási kísérlet egy NotSupportedException
.
Szükség szerint más kivételeket is megadhat, de ezek nem tartalmazzák automatikusan a JSON elérési útját.
Egyéni konverter regisztrálása
Regisztráljon egy egyéni konvertert, hogy a metódusok és Deserialize
a Serialize
metódusok használva legyenek. Válasszon az alábbi módszerek közül:
- Adja hozzá a konverterosztály egy példányát a JsonSerializerOptions.Converters gyűjteményhez.
- Alkalmazza a [JsonConverter] attribútumot az egyéni konvertert igénylő tulajdonságokra.
- Alkalmazza a [JsonConverter] attribútumot egy egyéni értéktípust képviselő osztályra vagy szerkezetre.
Regisztrációs minta – Konverterek gyűjteménye
Íme egy példa, amely a DateTimeOffsetJsonConvertert állítja be az alapértelmezett típustulajdonságok DateTimeOffsetközé:
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true,
Converters =
{
new DateTimeOffsetJsonConverter()
}
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);
Tegyük fel, hogy a következő típusú példányt szerializálja:
public class WeatherForecast
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Íme egy példa az egyéni konverter használatát szemléltető JSON-kimenetre:
{
"Date": "08/01/2019",
"TemperatureCelsius": 25,
"Summary": "Hot"
}
Az alábbi kód ugyanezt a módszert használja az egyéni DateTimeOffset
konverter használatával történő deszerializáláshoz:
var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions)!;
Regisztrációs minta – [JsonConverter] tulajdonságon
A következő kód kiválaszt egy egyéni konvertert a Date
tulajdonsághoz:
public class WeatherForecastWithConverterAttribute
{
[JsonConverter(typeof(DateTimeOffsetJsonConverter))]
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
A szerializálandó WeatherForecastWithConverterAttribute
kód nem igényli a következő használatát JsonSerializeOptions.Converters
:
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);
A deszerializálni kívánt kódhoz nem szükséges a következő használata Converters
:
weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithConverterAttribute>(jsonString)!;
Regisztrációs minta – [JsonConverter] típuson
Az alábbi kód létrehoz egy strukturát, és alkalmazza rá az [JsonConverter]
attribútumot:
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);
}
}
}
Az előző struktúra egyéni konvertere a következő:
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());
}
}
A [JsonConverter]
szerkezet attribútuma az egyéni konvertert regisztrálja alapértelmezettként a típustulajdonságok Temperature
esetében. A konverter automatikusan az TemperatureCelsius
alábbi típusú tulajdonságon lesz használva szerializálásakor vagy deszerializálásakor:
public class WeatherForecastWithTemperatureStruct
{
public DateTimeOffset Date { get; set; }
public Temperature TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Konverterregisztráció elsőbbsége
Szerializálás vagy deszerializálás során minden JSON-elemhez konvertert választunk a következő sorrendben, a legmagasabb prioritástól a legalacsonyabbig:
[JsonConverter]
tulajdonságra alkalmazva.- A gyűjteményhez
Converters
hozzáadott konverter. [JsonConverter]
egyéni értéktípusra vagy POCO-ra alkalmazva.
Ha egy típushoz több egyéni konverter van regisztrálva a Converters
gyűjteményben, akkor a rendszer az első konvertert használja, amely visszaadja true
azokat CanConvert
.
A beépített konvertereket csak akkor választja ki a rendszer, ha nincs regisztrálva a megfelelő egyéni konverter.
Konverterminták gyakori forgatókönyvekhez
A következő szakaszok olyan konvertermintákat nyújtanak, amelyek olyan gyakori forgatókönyveket kezelnek, amelyeket a beépített funkciók nem kezelnek.
- A kikövetkeztetett típusok deszerializálása objektumtulajdonságokra.
- Típusok esetén támogatja az oda-vissza utazást
Stack
. - Használja az alapértelmezett rendszerkonvertert.
- A kikövetkeztetett típusok deszerializálása objektumtulajdonságokra.
- Támogatja a polimorf deszerializálást.
- Típusok esetén támogatja az oda-vissza utazást
Stack
. - Elnevezési szabályzatok a szám sztring deszerializálásához.
Mintakonverter DataTable esetén lásd a támogatott gyűjteménytípusokat.
A kikövetkeztetett típusok deszerializálása objektumtulajdonságokra
Ha egy típustulajdonságra object
deszerializál, létrejön egy JsonElement
objektum. Ennek az az oka, hogy a deszerializáló nem tudja, milyen CLR-típust szeretne létrehozni, és nem próbálja kitalálni. Ha például egy JSON-tulajdonság értéke "true", akkor a deszerializáló nem arra következtet, hogy az érték egy Boolean
, és ha egy elem "2019. 01. 01." értékkel rendelkezik, akkor a deszerializáló nem arra következtet, hogy az egy DateTime
.
A típus következtetése pontatlan lehet. Ha a deszerializáló olyan JSON-számot elemez, amelynek nincs tizedesvesszője long
, az tartományon kívüli problémákat okozhat, ha az érték eredetileg szerializálva ulong
BigInteger
lett. Ha a szám eredetileg szerializálva decimal
lett, akkor a tizedesvesszővel double
rendelkező szám elemzése elveszítheti a pontosságot.
Típuskövetkeztetést igénylő forgatókönyvek esetén az alábbi kód egy egyéni konvertert jelenít meg a tulajdonságokhoz object
. A kód átalakítja a következőt:
true
ésfalse
aBoolean
- Tizedesjegy nélküli számok
long
- Számok tizedesjellel
double
- Dátumok
DateTime
- Sztringek a
string
- Minden máshoz
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"
//}
A példa a konverterkódot és a tulajdonságokat tartalmazó osztályt WeatherForecast
object
mutatja be. A Main
metódus először a konverter használata nélkül, majd a konverter használatával deszerializál egy JSON-sztringet egy WeatherForecast
példányba. A konzol kimenete azt mutatja, hogy a konverter nélkül a tulajdonság futásidejének típusa Date
; a konverter esetében a futási idő típusa DateTime
.JsonElement
A névtér egységtesztek mappájában további példák találhatók a System.Text.Json.Serialization
tulajdonságok deszerializálását object
kezelő egyéni konverterekre.
Polimorf deszerializálás támogatása
A .NET 7 támogatja a polimorf szerializálást és a deszerializálást is. A korábbi .NET-verziókban azonban korlátozott volt a polimorf szerializálás támogatása, és nem támogatott a deszerializálás. Ha .NET 6-ot vagy korábbi verziót használ, a deszerializáláshoz egyéni konverterre van szükség.
Tegyük fel például, hogy van egy Person
absztrakt alaposztálya, amelyből Employee
és Customer
származtatott osztályokból áll. A polimorf deszerializálás azt jelenti, hogy a tervezéskor megadható Person
deszerializálási célként, a Customer
Employee
JSON objektumai pedig futásidőben megfelelően deszerializálódnak. A deszerializálás során olyan nyomokat kell találnia, amelyek azonosítják a szükséges típust a JSON-ban. A rendelkezésre álló nyomok típusai az egyes forgatókönyvekben eltérőek. Előfordulhat például, hogy egy diszkriminatív tulajdonság elérhető, vagy egy adott tulajdonság jelenlétére vagy hiányára kell támaszkodnia. Az aktuális kiadás System.Text.Json
nem biztosít attribútumokat a polimorf deszerializálási forgatókönyvek kezeléséhez, ezért egyéni konverterekre van szükség.
Az alábbi kód egy alaposztályt, két származtatott osztályt és egy egyéni konvertert jelenít meg számukra. A konverter diszkriminatív tulajdonságot használ a polimorf deszerializáláshoz. A típuskriminatív nem szerepel az osztálydefiníciókban, de szerializáláskor jön létre, és a deszerializálás során olvassa be.
Fontos
A példakód megköveteli, hogy a JSON-objektumnév/érték párok sorrendben maradjanak, ami nem a JSON szabványkövetelménye.
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();
}
}
}
A következő kód regisztrálja a konvertert:
var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new PersonConverterWithTypeDiscriminator());
A konverter képes deszerializálni az azonos konverterrel létrehozott JSON-t a szerializáláshoz, például:
[
{
"TypeDiscriminator": 1,
"CreditLimit": 10000,
"Name": "John"
},
{
"TypeDiscriminator": 2,
"OfficeNumber": "555-1234",
"Name": "Nancy"
}
]
Az előző példában szereplő konverterkód manuálisan olvassa és írja be az egyes tulajdonságokat. Másik lehetőségként meghívhatja Deserialize
vagy Serialize
elvégezheti a munka egy részét. Például tekintse meg ezt a StackOverflow-bejegyzést.
Alternatív módszer a polimorf deszerializálásra
A metódusban Read
a következőt hívhatja Deserialize
meg:
- Klónozza a példányt
Utf8JsonReader
. MivelUtf8JsonReader
ez egy szerkezet, ez csak egy hozzárendelési utasítást igényel. - A klón használatával olvassa át a diszkriminatív jogkivonatokat.
- Hívja meg
Deserialize
az eredetiReader
példányt, ha már ismeri a kívánt típust. Hívhat,Deserialize
mert az eredetiReader
példány továbbra is a kezdő objektum jogkivonatának olvasásához van elhelyezve.
Ennek a módszernek a hátránya, hogy nem adhatja meg az eredeti beállításpéldányt, amely a konvertert Deserialize
regisztrálja. Ez egy verem túlcsordulását okozhatja, ahogyan azt a Szükséges tulajdonságok című témakörben ismertetik. Az alábbi példa egy Read
olyan módszert mutat be, amely ezt a alternatívát használja:
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;
}
Típusok esetén az Stack
oda-vissza utazás támogatása
Ha egy JSON-sztringet deszerializál egy Stack
objektumba, majd szerializálja azt, a verem tartalma fordított sorrendben van. Ez a viselkedés a következő típusokra és felületekre, valamint az ezekből származtatott felhasználó által definiált típusokra vonatkozik:
Az eredeti sorrendet a veremben megőrző szerializálás és deszerializálás támogatásához egyéni konverterre van szükség.
Az alábbi kód egy egyéni konvertert mutat be, amely lehetővé teszi az objektumokra és az objektumokról való Stack<T>
ciklikus bemásolást:
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();
}
}
}
A következő kód regisztrálja a konvertert:
var options = new JsonSerializerOptions
{
Converters = { new JsonConverterFactoryForStackOfT() },
};
Elnevezési szabályzatok enumerializáláshoz
Alapértelmezés szerint a beépített függvény JsonStringEnumConverter szerializálhatja és deszerializálhatja az enumerálás sztringértékeit. Egy megadott elnevezési szabályzat vagy az CamelCase elnevezési szabályzat nélkül működik. Nem támogatja az egyéb elnevezési szabályzatokat, például a kígyó esetét. Az egyéni konverterkóddal kapcsolatos információkért, amelyek támogatják az enumerálási sztringértékek közötti és az azokból való ciklikus be- és visszacsatolást egy kígyó-esetelnevezési szabályzat használatakor, tekintse meg a GitHub dotnet/runtime #31619 problémáját. Másik lehetőségként frissítsen a .NET 7 vagy újabb verzióira, amelyek beépített támogatást nyújtanak az elnevezési szabályzatok alkalmazásához az enumerálási sztringértékekre való ciklikus be- és visszacsatoláskor.
Alapértelmezett rendszerkonverter használata
Bizonyos esetekben érdemes lehet az alapértelmezett rendszerkonvertert használni egy egyéni konverterben. Ehhez kérje le a rendszerkonvertert a JsonSerializerOptions.Default tulajdonságból az alábbi példában látható módon:
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);
}
}
A null értékek kezelése
Alapértelmezés szerint a szerializáló a következőképpen kezeli a null értékeket:
Referenciatípusok és Nullable<T> típusok esetén:
- A szerializálás nem továbbítja
null
az egyéni konvertereket. - Nem adja át
JsonTokenType.Null
az egyéni konvertereknek a deszerializálást. - Deszerializálási példányt
null
ad vissza. - Közvetlenül az íróval szerializál
null
.
- A szerializálás nem továbbítja
Nem null értékű értéktípusok esetén:
- A deszerializálás egyéni konvertereinek továbbítja
JsonTokenType.Null
. (Ha nem érhető el egyéni konverter, a típus belső konvertere kivételtJsonException
jelez.)
- A deszerializálás egyéni konvertereinek továbbítja
Ez a nullkezelési viselkedés elsősorban a teljesítményt optimalizálja, ha kihagy egy extra hívást a konverternek. Emellett elkerüli a null értékű típusok konvertereinek kényszerítését, hogy minden Read
egyes metódus Write
felülbírálásakor ellenőrizze null
azokat.
Ha engedélyezni szeretné, hogy egy egyéni konverter egy referencia- vagy értéktípust kezeljen null
, felülbírálást JsonConverter<T>.HandleNull kell visszaadnia true
, ahogyan az alábbi példában látható:
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.
Hivatkozások megőrzése
Alapértelmezés szerint a referenciaadatok csak az egyes hívásokhoz vagy hívásokhoz gyorsítótárazva lesznek SerializeDeserialize. Ha meg szeretné őrizni a hivatkozásokat az egyik Serialize
/Deserialize
hívásból a másikba, gyökereztetheti a ReferenceResolver példányt a híváswebhelyen.Serialize
/Deserialize
Az alábbi kód egy példát mutat be erre a forgatókönyvre:
- A típushoz
Company
egyéni konvertert kell írnia. - Nem szeretné manuálisan szerializálni a
Supervisor
tulajdonságot, amely egyEmployee
. Ezt a szerializálónak szeretné delegálni, és meg szeretné őrizni a már mentett hivatkozásokat is.
Íme az osztályok és Company
az Employee
osztályok:
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; }
}
A konverter a következőképpen néz ki:
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();
}
}
Egy olyan osztály, amely a hivatkozások szótárban való tárolásából ReferenceResolver származik:
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;
}
}
Egy osztály, amely egy példányból ReferenceHandlerMyReferenceResolver
származik, és csak szükség esetén hoz létre új példányt (az ebben a példában elnevezett Reset
metódusban):
class MyReferenceHandler : ReferenceHandler
{
public MyReferenceHandler() => Reset();
private ReferenceResolver? _rootedResolver;
public override ReferenceResolver CreateResolver() => _rootedResolver!;
public void Reset() => _rootedResolver = new MyReferenceResolver();
}
Amikor a mintakód meghívja a szerializálót, egy olyan példányt JsonSerializerOptions használ, amelyben a ReferenceHandler tulajdonság egy példányra MyReferenceHandler
van állítva. Ha követi ezt a mintát, mindenképpen állítsa alaphelyzetbe a szótárat, amikor végzett a ReferenceResolver
szerializálással, hogy ne nőjön örökké.
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();
Az előző példa csak szerializálást végez, de hasonló megközelítés alkalmazható a deszerializáláshoz.
Egyéb egyéni konverterminták
A Migrálás a cikkből Newtonsoft.JsonSystem.Text.Json az egyéni konverterek további mintáit tartalmazza.
A forráskód egységtesztelési mappájaSystem.Text.Json.Serialization
más egyéni konvertermintákat is tartalmaz, például:
- Int32 konverter, amely null értéket 0-ra konvertál deszerializálás esetén
- Int32 konverter, amely lehetővé teszi a sztring- és számértékek deszerializálását
- Enum konverter
- Külső adatokat elfogadó T-konverter> listázása<
- Long[] konverter, amely vesszővel tagolt számlistával működik
Ha olyan konvertert kell készítenie, amely módosítja egy meglévő beépített konverter viselkedését, lekérheti a meglévő konverter forráskódját, hogy kiindulópontként szolgáljon a testreszabáshoz.