Aangepaste conversieprogramma's schrijven voor JSON-serialisatie (marshalling) in .NET
In dit artikel wordt beschreven hoe u aangepaste conversieprogramma's maakt voor de JSON-serialisatieklassen die zijn opgegeven in de System.Text.Json naamruimte. Zie JSON serialiseren en deserialiseren in .NET voor een inleiding System.Text.Json
tot.
Een conversieprogramma is een klasse die een object of een waarde converteert naar en van JSON. De System.Text.Json
naamruimte heeft ingebouwde conversieprogramma's voor de meeste primitieve typen die zijn toegewezen aan JavaScript-primitieven. U kunt aangepaste conversieprogramma's schrijven om het standaardgedrag van een ingebouwd conversieprogramma te overschrijven. Voorbeeld:
- Mogelijk wilt u waarden
DateTime
weergeven met de notatie mm/dd/jjjj. ISO 8601-1:2019 wordt standaard ondersteund, inclusief het RFC 3339-profiel. Zie de ondersteuning voor DateTime en DateTimeOffset in System.Text.Jsonvoor meer informatie. - Mogelijk wilt u een POCO serialiseren als JSON-tekenreeks, bijvoorbeeld met een
PhoneNumber
type.
U kunt ook aangepaste conversieprogramma's schrijven om aan te passen of uit te breiden System.Text.Json
met nieuwe functionaliteit. De volgende scenario's worden verderop in dit artikel behandeld:
- Afgeleide typen deserialiseren naar objecteigenschappen.
- Ondersteuning voor polymorf deserialisatie.
- Ondersteuning voor retouren voor
Stack
typen. - Gebruik het standaardsysteemconversieprogramma.
Visual Basic kan niet worden gebruikt om aangepaste conversieprogramma's te schrijven, maar kan conversieprogramma's aanroepen die zijn geïmplementeerd in C#-bibliotheken. Zie Visual Basic-ondersteuning voor meer informatie.
Aangepaste conversiepatronen
Er zijn twee patronen voor het maken van een aangepast conversieprogramma: het basispatroon en het fabriekspatroon. Het fabriekspatroon is bedoeld voor conversieprogramma's die type Enum
of open generics verwerken. Het basispatroon is bedoeld voor niet-algemene en gesloten algemene typen. Voor conversieprogramma's voor de volgende typen is bijvoorbeeld het fabriekspatroon vereist:
Enkele voorbeelden van typen die kunnen worden verwerkt door het basispatroon zijn:
Met het basispatroon wordt een klasse gemaakt die één type kan verwerken. Het factory-patroon maakt een klasse die bepaalt, tijdens runtime, welk specifiek type vereist is en dynamisch het juiste conversieprogramma maakt.
Voorbeeld van eenvoudige conversieprogramma
Het volgende voorbeeld is een conversieprogramma dat de standaardserialisatie voor een bestaand gegevenstype overschrijft. Het conversieprogramma maakt gebruik van mm-dd-/jjjj-indeling voor DateTimeOffset
eigenschappen.
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));
}
}
Voorbeeld van factorypatroonconversieprogramma
De volgende code toont een aangepast conversieprogramma waarmee wordt gebruikt Dictionary<Enum,TValue>
. De code volgt het factory-patroon omdat de eerste algemene typeparameter is Enum
en de tweede is geopend. De CanConvert
methode retourneert true
alleen voor een Dictionary
met twee algemene parameters, waarvan de eerste een Enum
type is. Het binnenste conversieprogramma krijgt een bestaand conversieprogramma voor het afhandelen van het type dat tijdens de uitvoering TValue
wordt opgegeven.
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();
}
}
}
}
Stappen om het basispatroon te volgen
In de volgende stappen wordt uitgelegd hoe u een conversieprogramma maakt door het basispatroon te volgen:
- Maak een klasse die is afgeleid van JsonConverter<T> waar
T
het type moet worden geserialiseerd en gedeserialiseerd. - Overschrijf de
Read
methode voor het deserialiseren van de binnenkomende JSON en converteer deze naar typeT
. Gebruik de Utf8JsonReader methode die wordt doorgegeven aan de methode om de JSON te lezen. U hoeft zich geen zorgen te maken over het verwerken van gedeeltelijke gegevens, omdat de serializer alle gegevens voor het huidige JSON-bereik doorgeeft. Het is dus niet nodig om aan te roepen Skip of TrySkip om te valideren dat Read retourneerttrue
. - Overschrijf de
Write
methode om het binnenkomende object van het typeT
te serialiseren. Gebruik de Utf8JsonWriter methode die wordt doorgegeven om de JSON te schrijven. - Overschrijf de
CanConvert
methode alleen indien nodig. De standaard implementatie retourneerttrue
wanneer het type van het typeT
is. Daarom hoeven conversieprogramma's die alleen het typeT
ondersteunen, deze methode niet te overschrijven. Zie de sectie polymorfische deserialisatie verderop in dit artikel voor een voorbeeld van een conversieprogramma dat deze methode moet overschrijven.
U kunt verwijzen naar de ingebouwde broncode voor conversieprogramma's als referentie-implementaties voor het schrijven van aangepaste conversieprogramma's.
Stappen om het factory-patroon te volgen
In de volgende stappen wordt uitgelegd hoe u een conversieprogramma maakt door het fabriekspatroon te volgen:
- Maak een klasse die is afgeleid van JsonConverterFactory.
- Overschrijf de
CanConvert
methode die moet worden geretourneerdtrue
wanneer het type dat moet worden geconverteerd een methode is die door het conversieprogramma kan worden verwerkt. Als het conversieprogramma bijvoorbeeld bedoeld is voorList<T>
, kan het alleen worden verwerktList<int>
,List<string>
enList<DateTime>
. - Overschrijf de
CreateConverter
methode om een exemplaar van een conversieklasse te retourneren waarmee het type-naar-conversie wordt verwerkt dat tijdens runtime wordt geleverd. - Maak de conversieklasse die door de
CreateConverter
methode wordt geïnstitueert.
Het factory-patroon is vereist voor open generics, omdat de code voor het converteren van een object naar en van een tekenreeks niet hetzelfde is voor alle typen. Een conversieprogramma voor een open algemeen type (List<T>
bijvoorbeeld) moet achter de schermen een conversieprogramma maken voor een gesloten algemeen type (List<DateTime>
bijvoorbeeld). Code moet worden geschreven om elk gesloten algemeen type te verwerken dat het conversieprogramma kan verwerken.
Het Enum
type is vergelijkbaar met een open algemeen type: achter de schermen moet een conversieprogramma voor een specifiek Enum
(WeekdaysEnum
bijvoorbeeld) conversieprogramma Enum
worden gemaakt.
Het gebruik van Utf8JsonReader
in de Read
methode
Als uw conversieprogramma een JSON-object converteert, wordt het Utf8JsonReader
geplaatst op het beginobjecttoken wanneer de Read
methode begint. Vervolgens moet u alle tokens in dat object lezen en de methode afsluiten met de lezer op het bijbehorende eindobjecttoken. Als u verder leest dan het einde van het object of als u stopt voordat u het bijbehorende eindtoken bereikt, krijgt u een JsonException
uitzondering die aangeeft dat:
Het conversieprogramma 'ConverterName' leest te veel of niet genoeg.
Zie het bovenstaande voorbeeldconversieprogramma voor fabriekspatronen voor een voorbeeld. De Read
methode begint door te controleren of de lezer op een beginobjecttoken is geplaatst. Het wordt gelezen totdat wordt gevonden dat het op het volgende eindpuntobjecttoken wordt geplaatst. Het stopt op het volgende eindobjecttoken omdat er geen tussenliggende beginobjecttokens zijn die een object binnen het object aangeven. Dezelfde regel over het begintoken en het eindtoken is van toepassing als u een matrix converteert. Zie het Stack<T>
voorbeeldconversieprogramma verderop in dit artikel voor een voorbeeld.
Foutafhandeling
De serializer biedt speciale verwerking voor uitzonderingstypen JsonException en NotSupportedException.
JsonException
Als u een JsonException
bericht zonder bericht genereert, wordt met de serializer een bericht gemaakt dat het pad naar het deel van de JSON bevat dat de fout heeft veroorzaakt. De instructie throw new JsonException()
produceert bijvoorbeeld een foutbericht zoals in het volgende voorbeeld:
Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.
Als u wel een bericht opgeeft (bijvoorbeeld throw new JsonException("Error occurred")
), stelt de serializer nog steeds de Path, LineNumberen BytePositionInLine eigenschappen in.
NotSupportedException
Als u een NotSupportedException
bericht genereert, krijgt u altijd de padinformatie in het bericht. Als u een bericht opgeeft, wordt de padgegevens eraan toegevoegd. De instructie throw new NotSupportedException("Error occurred.")
produceert bijvoorbeeld een foutbericht zoals in het volgende voorbeeld:
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
Wanneer te gooien welk uitzonderingstype
Wanneer de JSON-nettolading tokens bevat die niet geldig zijn voor het type dat wordt gedeserialiseerd, genereert u een JsonException
.
Als je bepaalde typen niet wilt weigeren, gooi dan een NotSupportedException
. Deze uitzondering is wat de serializer automatisch genereert voor typen die niet worden ondersteund. Wordt bijvoorbeeld System.Type
om veiligheidsredenen niet ondersteund, dus een poging om deze te deserialiseren resulteert in een NotSupportedException
.
U kunt indien nodig andere uitzonderingen genereren, maar ze bevatten niet automatisch informatie over het JSON-pad.
Een aangepast conversieprogramma registreren
Registreer een aangepast conversieprogramma om de Serialize
en Deserialize
methoden te gebruiken. Kies een van de volgende methoden:
- Voeg een exemplaar van de conversieklasse toe aan de JsonSerializerOptions.Converters verzameling.
- Pas het kenmerk [Json Converter] toe op de eigenschappen waarvoor het aangepaste conversieprogramma is vereist.
- Pas het kenmerk [JsonConverter] toe op een klasse of een struct die een aangepast waardetype vertegenwoordigt.
Registratievoorbeeld - Verzameling conversieprogramma's
Hier volgt een voorbeeld waarmee de DateTimeOffsetJsonConverter de standaardinstelling is voor eigenschappen van het type DateTimeOffset:
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true
};
serializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);
Stel dat u een exemplaar van het volgende type serialiseert:
public class WeatherForecast
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Hier volgt een voorbeeld van JSON-uitvoer waarin wordt weergegeven dat het aangepaste conversieprogramma is gebruikt:
{
"Date": "08/01/2019",
"TemperatureCelsius": 25,
"Summary": "Hot"
}
De volgende code maakt gebruik van dezelfde methode om het deserialiseren te deserialiseren met behulp van het aangepaste DateTimeOffset
conversieprogramma:
var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions)!;
Registratievoorbeeld - [JsonConverter] op een eigenschap
De volgende code selecteert een aangepast conversieprogramma voor de Date
eigenschap:
public class WeatherForecastWithConverterAttribute
{
[JsonConverter(typeof(DateTimeOffsetJsonConverter))]
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Voor de code die moet worden geserialiseerd WeatherForecastWithConverterAttribute
, is het gebruik van JsonSerializeOptions.Converters
:
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);
De code die moet worden gedeserialiseerd, vereist ook niet het gebruik van Converters
:
weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithConverterAttribute>(jsonString)!;
Registratievoorbeeld - [JsonConverter] op een type
Hier volgt code waarmee een struct wordt gemaakt en het [JsonConverter]
kenmerk hierop wordt toegepast:
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);
}
}
}
Dit is het aangepaste conversieprogramma voor de voorgaande struct:
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());
}
}
Het [JsonConverter]
kenmerk in de struct registreert het aangepaste conversieprogramma als de standaardwaarde voor eigenschappen van het type Temperature
. Het conversieprogramma wordt automatisch gebruikt voor de TemperatureCelsius
eigenschap van het volgende type wanneer u deze serialiseert of deserialiseert:
public class WeatherForecastWithTemperatureStruct
{
public DateTimeOffset Date { get; set; }
public Temperature TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Prioriteit van conversieprogrammaregistratie
Tijdens serialisatie of deserialisatie wordt een conversieprogramma gekozen voor elk JSON-element in de volgende volgorde, weergegeven van hoogste prioriteit tot laagste:
[JsonConverter]
toegepast op een eigenschap.- Er is een conversieprogramma toegevoegd aan de
Converters
verzameling. [JsonConverter]
toegepast op een aangepast waardetype of POCO.
Als er meerdere aangepaste conversieprogramma's voor een type zijn geregistreerd in de Converters
verzameling, wordt het eerste conversieprogramma gebruikt dat wordt geretourneerd true
CanConvert
.
Een ingebouwd conversieprogramma wordt alleen gekozen als er geen toepasselijke aangepaste conversieprogramma is geregistreerd.
Conversieprogrammavoorbeelden voor veelvoorkomende scenario's
De volgende secties bevatten conversieprogrammavoorbeelden die betrekking hebben op enkele veelvoorkomende scenario's die niet worden verwerkt met ingebouwde functionaliteit.
- Afgeleide typen deserialiseren naar objecteigenschappen.
- Ondersteuning voor retouren voor
Stack
typen. - Gebruik het standaardsysteemconversieprogramma.
Zie Ondersteunde verzamelingstypen voor een voorbeeldconversieprogrammaDataTable.
Uitgestelde typen deserialiseren naar objecteigenschappen
Wanneer een eigenschap van het type object
wordt gedeserialiseerd, wordt er een JsonElement
object gemaakt. De reden hiervoor is dat de deserializer niet weet welk CLR-type moet worden gemaakt en niet probeert te raden. Als een JSON-eigenschap bijvoorbeeld 'true' heeft, wordt door de deserializer niet afgeleid dat de waarde een Boolean
, en als een element '01/01/2019' heeft, wordt door de deserializer niet afgeleid dat het een DateTime
.
Typedeductie kan onnauwkeurig zijn. Als de deserializer een JSON-getal parseert dat geen decimaalteken heeft als een long
, kan dit leiden tot problemen buiten het bereik als de waarde oorspronkelijk is geserialiseerd als een ulong
of BigInteger
. Het parseren van een getal met een decimaalteken als een double
getal kan de precisie verliezen als het getal oorspronkelijk is geserialiseerd als een decimal
.
Voor scenario's waarvoor typedeductie is vereist, toont de volgende code een aangepast conversieprogramma voor object
eigenschappen. De code converteert:
true
enfalse
naarBoolean
- Getallen zonder een decimaal getal tot
long
- Getallen met een decimaal getal tot
double
- Datums tot
DateTime
- Tekenreeksen naar
string
- Alles wat u nog meer wilt
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"
//}
In het voorbeeld ziet u de conversiecode en een WeatherForecast
klasse met object
eigenschappen. Met Main
de methode wordt een JSON-tekenreeks gedeserialiseerd in een WeatherForecast
exemplaar, eerst zonder het conversieprogramma te gebruiken en vervolgens het conversieprogramma te gebruiken. De console-uitvoer laat zien dat zonder het conversieprogramma het runtimetype voor de Date
eigenschap is JsonElement
; met het conversieprogramma is het runtimetype DateTime
.
De map eenheidstests in de System.Text.Json.Serialization
naamruimte bevat meer voorbeelden van aangepaste conversieprogramma's die deserialisatie naar object
eigenschappen verwerken.
Ondersteuning voor polymorfe deserialisatie
.NET 7 biedt ondersteuning voor zowel polymorfe serialisatie als deserialisatie. In eerdere .NET-versies was er echter beperkte ondersteuning voor polymorfe serialisatie en geen ondersteuning voor deserialisatie. Als u .NET 6 of een eerdere versie gebruikt, is voor deserialisatie een aangepast conversieprogramma vereist.
Stel dat u een Person
abstracte basisklasse hebt met Employee
en Customer
afgeleide klassen. Polymorf deserialisatie betekent dat u tijdens het ontwerp kunt opgeven Person
als het deserialisatiedoel en Customer
dat Employee
objecten in de JSON correct worden gedeserialiseerd tijdens runtime. Tijdens de deserialisatie moet u aanwijzingen vinden die het vereiste type in de JSON identificeren. De soorten aanwijzingen die beschikbaar zijn, variëren per scenario. Een discriminerende eigenschap kan bijvoorbeeld beschikbaar zijn of u moet afhankelijk zijn van de aanwezigheid of afwezigheid van een bepaalde eigenschap. De huidige release van System.Text.Json
bevat geen kenmerken om op te geven hoe polymorfe deserialisatiescenario's moeten worden verwerkt, zodat aangepaste conversieprogramma's vereist zijn.
De volgende code toont een basisklasse, twee afgeleide klassen en een aangepast conversieprogramma. Het conversieprogramma gebruikt een discriminatoreigenschap om polymorfische deserialisatie uit te voeren. Het typediscriminatie bevindt zich niet in de klassedefinities, maar wordt gemaakt tijdens serialisatie en wordt gelezen tijdens deserialisatie.
Belangrijk
Voor de voorbeeldcode moeten JSON-objectnaam-waardeparen op volgorde blijven staan. Dit is geen standaardvereiste van 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();
}
}
}
Met de volgende code wordt het conversieprogramma geregistreerd:
var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new PersonConverterWithTypeDiscriminator());
Het conversieprogramma kan JSON deserialiseren die is gemaakt met behulp van hetzelfde conversieprogramma om te serialiseren, bijvoorbeeld:
[
{
"TypeDiscriminator": 1,
"CreditLimit": 10000,
"Name": "John"
},
{
"TypeDiscriminator": 2,
"OfficeNumber": "555-1234",
"Name": "Nancy"
}
]
De conversiecode in het voorgaande voorbeeld leest en schrijft elke eigenschap handmatig. Een alternatief is het bellen Deserialize
of Serialize
doen van een deel van het werk. Zie dit StackOverflow-bericht voor een voorbeeld.
Een alternatieve manier om polymorfe deserialisatie uit te voeren
U kunt de Read
methode aanroepenDeserialize
:
- Maak een kloon van het
Utf8JsonReader
exemplaar. OmdatUtf8JsonReader
dit een struct is, is hiervoor alleen een toewijzingsinstructie vereist. - Gebruik de kloon om de discriminatortokens te lezen.
- Roep
Deserialize
het oorspronkelijkeReader
exemplaar aan wanneer u weet welk type u nodig hebt. U kunt aanroepenDeserialize
omdat het oorspronkelijkeReader
exemplaar nog steeds is geplaatst om het beginobjecttoken te lezen.
Een nadeel van deze methode is dat u niet kunt doorgeven in het oorspronkelijke optiesexemplaren waarop het conversieprogramma Deserialize
wordt geregistreerd. Als u dit doet, wordt een stack-overloop veroorzaakt, zoals wordt uitgelegd in de vereiste eigenschappen. In het volgende voorbeeld ziet u een Read
methode die gebruikmaakt van dit alternatief:
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;
}
Ondersteuning voor retouren voor Stack
typen
Als u een JSON-tekenreeks deserialiseert in een Stack
object en dat object vervolgens serialiseert, bevindt de inhoud van de stack zich in omgekeerde volgorde. Dit gedrag is van toepassing op de volgende typen en interfaces en door de gebruiker gedefinieerde typen die hiervan zijn afgeleid:
Ter ondersteuning van serialisatie en deserialisatie die de oorspronkelijke volgorde in de stack behoudt, is een aangepast conversieprogramma vereist.
De volgende code toont een aangepast conversieprogramma waarmee retourneert naar en van Stack<T>
objecten:
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();
}
}
}
Met de volgende code wordt het conversieprogramma geregistreerd:
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonConverterFactoryForStackOfT());
Standaardsysteemconversieprogramma gebruiken
In sommige scenario's wilt u mogelijk het standaardsysteemconversieprogramma gebruiken in een aangepast conversieprogramma. Hiervoor haalt u het systeemconversieprogramma op uit de JsonSerializerOptions.Default eigenschap, zoals wordt weergegeven in het volgende voorbeeld:
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);
}
}
Omgaan met null-waarden
Standaard verwerkt de serializer null-waarden als volgt:
Voor referentietypen en Nullable<T> -typen:
- Het wordt niet doorgegeven
null
aan aangepaste conversieprogramma's bij serialisatie. - Het wordt niet doorgegeven
JsonTokenType.Null
aan aangepaste conversieprogramma's bij deserialisatie. - Het retourneert een
null
exemplaar bij deserialisatie. - Het schrijft
null
rechtstreeks met de schrijver over serialisatie.
- Het wordt niet doorgegeven
Voor niet-null-waardetypen:
- Het wordt doorgegeven
JsonTokenType.Null
aan aangepaste conversieprogramma's bij deserialisatie. (Als er geen aangepast conversieprogramma beschikbaar is, wordt er eenJsonException
uitzondering gegenereerd door het interne conversieprogramma voor het type.)
- Het wordt doorgegeven
Dit gedrag voor null-verwerking is voornamelijk bedoeld om de prestaties te optimaliseren door een extra aanroep naar het conversieprogramma over te slaan. Bovendien wordt voorkomen dat conversieprogramma's voor null-typen worden afgedwongen om aan het begin van elke Read
en Write
methode-onderdrukking te controlerennull
.
Als u wilt dat een aangepast conversieprogramma kan worden verwerkt null
voor een verwijzing of waardetype, moet u overschrijven JsonConverter<T>.HandleNull om te retourneren true
, zoals wordt weergegeven in het volgende voorbeeld:
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.
Verwijzingen behouden
Referentiegegevens worden standaard alleen in de cache opgeslagen voor elke aanroep naar Serialize of Deserialize. Als u verwijzingen van de ene Serialize
/Deserialize
aanroep naar de andere wilt behouden, moet u het ReferenceResolver exemplaar in de aanroepsite van .Serialize
/Deserialize
De volgende code toont een voorbeeld voor dit scenario:
- U schrijft een aangepast conversieprogramma voor het
Company
type. - U wilt de
Supervisor
eigenschap niet handmatig serialiseren, eenEmployee
. U wilt dit delegeren aan de serializer en u wilt ook de verwijzingen behouden die u al hebt opgeslagen.
Dit zijn de Employee
en Company
klassen:
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; }
}
Het conversieprogramma ziet er als volgt uit:
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();
}
}
Een klasse die is afgeleid van de opslag van ReferenceResolver de verwijzingen in een woordenlijst:
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;
}
}
Een klasse die is afgeleid van ReferenceHandler een exemplaar van MyReferenceResolver
en maakt alleen een nieuw exemplaar wanneer dat nodig is (in een methode die in dit voorbeeld wordt genoemd Reset
):
class MyReferenceHandler : ReferenceHandler
{
public MyReferenceHandler() => Reset();
private ReferenceResolver? _rootedResolver;
public override ReferenceResolver CreateResolver() => _rootedResolver!;
public void Reset() => _rootedResolver = new MyReferenceResolver();
}
Wanneer de voorbeeldcode de serializer aanroept, wordt er een JsonSerializerOptions exemplaar gebruikt waarin de ReferenceHandler eigenschap is ingesteld op een exemplaar van MyReferenceHandler
. Wanneer u dit patroon volgt, moet u de ReferenceResolver
woordenlijst opnieuw instellen wanneer u klaar bent met serialiseren, zodat u het niet voor altijd kunt laten groeien.
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();
In het voorgaande voorbeeld wordt alleen serialisatie uitgevoerd, maar een vergelijkbare benadering kan worden gebruikt voor deserialisatie.
Andere voorbeelden van aangepaste conversieprogramma's
Het artikel Migreren van Newtonsoft.Json naar System.Text.Json artikel bevat aanvullende voorbeelden van aangepaste conversieprogramma's.
De map eenheidstests in de System.Text.Json.Serialization
broncode bevat andere aangepaste conversieprogrammavoorbeelden, zoals:
- Int32-conversieprogramma dat null converteert naar 0 bij deserialiseren
- Int32-conversieprogramma waarmee zowel tekenreeks- als getalwaarden kunnen worden gedeserialiseerd
- Enum-conversieprogramma
- Lijst<T-conversieprogramma> dat externe gegevens accepteert
- Lang[] conversieprogramma dat werkt met een door komma's gescheiden lijst met getallen
Als u een conversieprogramma wilt maken dat het gedrag van een bestaand ingebouwd conversieprogramma wijzigt, kunt u de broncode van het bestaande conversieprogramma ophalen om te fungeren als uitgangspunt voor aanpassing.