Så här skriver du anpassade konverterare för JSON-serialisering (marshalling) i .NET
Den här artikeln visar hur du skapar anpassade konverterare för JSON-serialiseringsklasserna som tillhandahålls i System.Text.Json namnområdet. En introduktion till System.Text.Json
finns i Serialisera och deserialisera JSON i .NET.
En konverterare är en klass som konverterar ett objekt eller ett värde till och från JSON. Namnområdet System.Text.Json
har inbyggda konverterare för de flesta primitiva typer som mappar till JavaScript-primitiver. Du kan skriva anpassade konverterare för att åsidosätta standardbeteendet för en inbyggd konverterare. Till exempel:
- Du kanske vill
DateTime
att värden ska representeras av formatet mm/dd/åååå. Som standard stöds ISO 8601-1:2019, inklusive RFC 3339-profilen. Mer information finns i Stöd för DateTime och DateTimeOffset i System.Text.Json. - Du kanske vill serialisera en POCO som JSON-sträng, till exempel med en
PhoneNumber
typ.
Du kan också skriva anpassade konverterare för att anpassa eller utöka System.Text.Json
med nya funktioner. Följande scenarier beskrivs senare i den här artikeln:
- Deserialisera uppskjutna typer till objektegenskaper.
- Stöd för polymorf deserialisering.
- Stöd tur och retur för
Stack
typer. - Använd standardsystemkonverteraren.
Visual Basic kan inte användas för att skriva anpassade konverterare, men kan anropa konverterare som implementeras i C#-bibliotek. Mer information finns i Visual Basic-stöd.
Anpassade konverterarmönster
Det finns två mönster för att skapa en anpassad konverterare: det grundläggande mönstret och fabriksmönstret. Fabriksmönstret är för konverterare som hanterar typ Enum
eller öppnar generiska objekt. Det grundläggande mönstret är för icke-generiska och stängda generiska typer. Konverterare för följande typer kräver till exempel fabriksmönstret:
Några exempel på typer som kan hanteras av det grundläggande mönstret är:
Det grundläggande mönstret skapar en klass som kan hantera en typ. Fabriksmönstret skapar en klass som vid körning avgör vilken specifik typ som krävs och dynamiskt skapar lämplig konverterare.
Grundläggande exempelkonverterare
Följande exempel är en konverterare som åsidosätter standard serialisering för en befintlig datatyp. Konverteraren använder formatet mm/dd/åååå för DateTimeOffset
egenskaper.
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));
}
}
Exempel på fabriksmönsterkonverterare
Följande kod visar en anpassad konverterare som fungerar med Dictionary<Enum,TValue>
. Koden följer fabriksmönstret eftersom den första generiska typparametern är Enum
och den andra är öppen. Metoden CanConvert
returnerar true
endast för en Dictionary
med två generiska parametrar, varav den första är en Enum
typ. Den inre konverteraren hämtar en befintlig konverterare för att hantera den typ som anges vid körning för 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();
}
}
}
}
Steg för att följa det grundläggande mönstret
Följande steg förklarar hur du skapar en konverterare genom att följa det grundläggande mönstret:
- Skapa en klass som härleds från JsonConverter<T> var
T
är den typ som ska serialiseras och deserialiseras. Read
Åsidosätt metoden för att deserialisera inkommande JSON och konvertera den till att skrivaT
. Använd den Utf8JsonReader som skickas till metoden för att läsa JSON. Du behöver inte bekymra dig om att hantera partiella data eftersom serialiseraren skickar alla data för det aktuella JSON-omfånget. Därför är det inte nödvändigt att anropa Skip eller verifiera att Read returnerartrue
TrySkip .- Åsidosätt
Write
metoden för att serialisera det inkommande objektet av typenT
. Använd som Utf8JsonWriter skickas till metoden för att skriva JSON. - Åsidosätt
CanConvert
metoden endast om det behövs. Standardimplementeringen returnerastrue
när typen som ska konverteras är av typenT
. Konverterare som endast stöder typenT
behöver därför inte åsidosätta den här metoden. Ett exempel på en konverterare som behöver åsidosätta den här metoden finns i avsnittet polymorf deserialisering senare i den här artikeln.
Du kan referera till den inbyggda konverterarens källkod som referensimplementeringar för att skriva anpassade konverterare.
Steg för att följa fabriksmönstret
Följande steg förklarar hur du skapar en konverterare genom att följa fabriksmönstret:
- Skapa en klass som härleds från JsonConverterFactory.
- Åsidosätt metoden
CanConvert
för att returneratrue
när typen som ska konverteras är en som konverteraren kan hantera. Om konverteraren till exempel är förList<T>
kan den bara hanteraList<int>
,List<string>
ochList<DateTime>
. - Åsidosätt
CreateConverter
metoden för att returnera en instans av en konverterarklass som hanterar den typ-till-konvertera som tillhandahålls vid körning. - Skapa den konverterarklass som
CreateConverter
metoden instansierar.
Fabriksmönstret krävs för öppna generiska objekt eftersom koden för att konvertera ett objekt till och från en sträng inte är samma för alla typer. En konverterare för en öppen allmän typ (List<T>
till exempel) måste skapa en konverterare för en sluten allmän typ (List<DateTime>
till exempel) i bakgrunden. Koden måste skrivas för att hantera varje sluten-generisk typ som konverteraren kan hantera.
Typen Enum
liknar en öppen allmän typ: en konverterare för Enum
måste skapa en konverterare för en specifik Enum
(WeekdaysEnum
till exempel) bakom kulisserna.
Användningen av Utf8JsonReader
i Read
-metoden
Om konverteraren konverterar ett JSON-objekt Utf8JsonReader
placeras den på startobjekttoken när Read
metoden börjar. Du måste sedan läsa igenom alla token i objektet och avsluta metoden med läsaren placerad på motsvarande slutobjekttoken. Om du läser bortom slutet av objektet, eller om du slutar innan du når motsvarande sluttoken, får du ett JsonException
undantag som anger att:
Konverteraren "ConverterName" läste för mycket eller inte tillräckligt.
Ett exempel finns i föregående exempelkonverterare för fabriksmönster. Metoden Read
börjar med att verifiera att läsaren är placerad på en startobjekttoken. Den läser tills den hittar att den är placerad på nästa slutobjekttoken. Den stoppas på nästa slutobjekttoken eftersom det inte finns några mellanliggande startobjekttoken som indikerar ett objekt i objektet. Samma regel om starttoken och sluttoken gäller om du konverterar en matris. Ett exempel finns i Stack<T>
exempelkonverteraren senare i den här artikeln.
Felhantering
Serialiseraren tillhandahåller särskild hantering för undantagstyper JsonException och NotSupportedException.
JsonException
Om du genererar ett JsonException
utan meddelande skapar serialiseraren ett meddelande som innehåller sökvägen till den del av JSON som orsakade felet. Instruktionen throw new JsonException()
genererar till exempel ett felmeddelande som i följande exempel:
Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.
Om du anger ett meddelande (till exempel throw new JsonException("Error occurred")
), anger Pathserialiseraren fortfarande egenskaperna , LineNumberoch BytePositionInLine .
NotSupportedException
Om du genererar en NotSupportedException
får du alltid sökvägsinformationen i meddelandet. Om du anger ett meddelande läggs sökvägsinformationen till. Instruktionen throw new NotSupportedException("Error occurred.")
genererar till exempel ett felmeddelande som i följande exempel:
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
När du ska utlösa vilken undantagstyp
När JSON-nyttolasten innehåller token som inte är giltiga för den typ som deserialiseras genererar du en JsonException
.
När du inte vill tillåta vissa typer genererar du en NotSupportedException
. Det här undantaget är vad serialiseraren automatiskt genererar för typer som inte stöds. Till exempel System.Type
stöds inte av säkerhetsskäl, så ett försök att deserialisera det resulterar i en NotSupportedException
.
Du kan utlösa andra undantag efter behov, men de innehåller inte automatiskt JSON-sökvägsinformation.
Registrera en anpassad konverterare
Registrera en anpassad konverterare för att få Serialize
metoderna och Deserialize
att använda den. Välj någon av följande metoder:
- Lägg till en instans av konverterarklassen i JsonSerializerOptions.Converters samlingen.
- Använd attributet [JsonConverter] för de egenskaper som kräver den anpassade konverteraren.
- Använd attributet [JsonConverter] för en klass eller en struct som representerar en anpassad värdetyp.
Registreringsexempel – Konverterarsamling
Här är ett exempel som gör DateTimeOffsetJsonConverter till standard för egenskaper av typen DateTimeOffset:
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true
};
serializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);
Anta att du serialiserar en instans av följande typ:
public class WeatherForecast
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Här är ett exempel på JSON-utdata som visar att den anpassade konverteraren användes:
{
"Date": "08/01/2019",
"TemperatureCelsius": 25,
"Summary": "Hot"
}
Följande kod använder samma metod för att deserialisera med den anpassade DateTimeOffset
konverteraren:
var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions)!;
Registreringsexempel – [JsonConverter] på en egenskap
Följande kod väljer en anpassad konverterare för Date
egenskapen:
public class WeatherForecastWithConverterAttribute
{
[JsonConverter(typeof(DateTimeOffsetJsonConverter))]
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Koden för serialisering WeatherForecastWithConverterAttribute
kräver inte användning av JsonSerializeOptions.Converters
:
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);
Koden för att deserialisera kräver inte heller användning av Converters
:
weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithConverterAttribute>(jsonString)!;
Registreringsexempel – [JsonConverter] på en typ
Här är kod som skapar en struct och tillämpar [JsonConverter]
attributet på den:
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);
}
}
}
Här är den anpassade konverteraren för föregående 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());
}
}
Attributet [JsonConverter]
på struct registrerar den anpassade konverteraren som standard för egenskaper av typen Temperature
. Konverteraren används automatiskt på TemperatureCelsius
egenskapen av följande typ när du serialiserar eller deserialiserar den:
public class WeatherForecastWithTemperatureStruct
{
public DateTimeOffset Date { get; set; }
public Temperature TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Registreringspriorence för konverterare
Under serialisering eller deserialisering väljs en konverterare för varje JSON-element i följande ordning, som anges från högsta prioritet till lägsta:
[JsonConverter]
tillämpas på en egenskap.- En konverterare har lagts till i
Converters
samlingen. [JsonConverter]
tillämpas på en anpassad värdetyp eller POCO.
Om flera anpassade konverterare för en typ registreras i Converters
samlingen används den första konverteraren som returneras true
för CanConvert
.
En inbyggd konverterare väljs endast om ingen tillämplig anpassad konverterare har registrerats.
Konverterarexempel för vanliga scenarier
Följande avsnitt innehåller konverterarexempel som hanterar några vanliga scenarier som inbyggda funktioner inte hanterar.
- Deserialisera uppskjutna typer till objektegenskaper.
- Stöd tur och retur för
Stack
typer. - Använd standardsystemkonverteraren.
En exempelkonverterare DataTable finns i Samlingstyper som stöds.
Deserialisera uppskjutna typer till objektegenskaper
När deserialisera till en egenskap av typen object
skapas ett JsonElement
objekt. Anledningen är att deserialiseraren inte vet vilken CLR-typ som ska skapas, och den försöker inte gissa. Om en JSON-egenskap till exempel har "true" kommer deserialiseraren inte att dra slutsatsen att värdet är en Boolean
, och om ett element har "01/01/2019" kommer deserialiseraren inte att dra slutsatsen att det är en DateTime
.
Typinferens kan vara felaktig. Om deserialiseraren parsar ett JSON-tal som inte har någon decimalpunkt som en long
, kan det resultera i out-of-range-problem om värdet ursprungligen serialiserades som en ulong
eller BigInteger
. Parsning av ett tal som har en decimalpunkt som en double
kan förlora precision om talet ursprungligen serialiserades som en decimal
.
För scenarier som kräver typinferens visar följande kod en anpassad konverterare för object
egenskaper. Koden konverterar:
true
ochfalse
för attBoolean
- Tal utan decimal till
long
- Tal med decimaler till
double
- Datum till
DateTime
- Strängar till
string
- Allt annat att
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"
//}
Exemplet visar konverterarkoden och en WeatherForecast
klass med object
egenskaper. Metoden Main
deserialiserar en JSON-sträng till en WeatherForecast
instans, först utan att använda konverteraren och sedan med konverteraren. Konsolens utdata visar att utan konverteraren är JsonElement
körningstypen för Date
egenskapen . Med konverteraren är DateTime
körningstypen .
Mappen enhetstest i System.Text.Json.Serialization
namnområdet innehåller fler exempel på anpassade konverterare som hanterar deserialisering till object
egenskaper.
Stöd för polymorf deserialisering
.NET 7 har stöd för både polymorf serialisering och deserialisering. I tidigare .NET-versioner fanns det dock begränsat stöd för polymorf serialisering och inget stöd för deserialisering. Om du använder .NET 6 eller en tidigare version kräver deserialisering en anpassad konverterare.
Anta till exempel att du har en Person
abstrakt basklass med Employee
och Customer
härledda klasser. Polymorf deserialisering innebär att du vid designtillfället kan ange Person
som mål för deserialisering och Customer
att Employee
objekt i JSON är korrekt deserialiserade vid körning. Under deserialiseringen måste du hitta ledtrådar som identifierar den typ som krävs i JSON. Vilka typer av ledtrådar som är tillgängliga varierar med varje scenario. Till exempel kan en diskriminerande egenskap vara tillgänglig eller så kan du behöva förlita dig på förekomsten eller frånvaron av en viss egenskap. Den aktuella versionen av System.Text.Json
innehåller inte attribut för att ange hur polymorfa deserialiseringsscenarier ska hanteras, så anpassade konverterare krävs.
Följande kod visar en basklass, två härledda klasser och en anpassad konverterare för dem. Konverteraren använder en diskriminerande egenskap för att utföra polymorf deserialisering. Typdiskriminatorn finns inte i klassdefinitionerna utan skapas under serialiseringen och läss under deserialiseringen.
Viktigt!
Exempelkoden kräver JSON-objektnamn/värdepar för att hålla sig i ordning, vilket inte är ett standardkrav för 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();
}
}
}
Följande kod registrerar konverteraren:
var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new PersonConverterWithTypeDiscriminator());
Konverteraren kan deserialisera JSON som skapades med hjälp av samma konverterare för att serialisera, till exempel:
[
{
"TypeDiscriminator": 1,
"CreditLimit": 10000,
"Name": "John"
},
{
"TypeDiscriminator": 2,
"OfficeNumber": "555-1234",
"Name": "Nancy"
}
]
Konverterarkoden i föregående exempel läser och skriver varje egenskap manuellt. Ett alternativ är att anropa Deserialize
eller Serialize
utföra en del av arbetet. Ett exempel finns i det här StackOverflow-inlägget.
Ett alternativt sätt att utföra polymorf deserialisering
Du kan anropa Deserialize
metoden Read
:
- Skapa en klon av instansen
Utf8JsonReader
. EftersomUtf8JsonReader
är en struct kräver detta bara en tilldelningsinstruktuering. - Använd klonen för att läsa igenom diskriminerande token.
- Anropa
Deserialize
med den ursprungligaReader
instansen när du vet vilken typ du behöver. Du kan anropaDeserialize
eftersom den ursprungligaReader
instansen fortfarande är placerad för att läsa startobjekttoken.
En nackdel med den här metoden är att du inte kan skicka in den ursprungliga alternativinstansen som registrerar konverteraren till Deserialize
. Detta skulle orsaka ett stackspill, enligt beskrivningen i Obligatoriska egenskaper. I följande exempel visas en Read
metod som använder det här alternativet:
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;
}
Support tur och retur för Stack
typer
Om du deserialiserar en JSON-sträng till ett Stack
objekt och sedan serialiserar objektet är innehållet i stacken i omvänd ordning. Det här beteendet gäller för följande typer och gränssnitt och användardefinierade typer som härleds från dem:
För att stödja serialisering och deserialisering som behåller den ursprungliga ordningen i stacken krävs en anpassad konverterare.
Följande kod visar en anpassad konverterare som möjliggör rund-tripping till och från 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();
}
}
}
Följande kod registrerar konverteraren:
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonConverterFactoryForStackOfT());
Använda standardsystemkonverterare
I vissa scenarier kanske du vill använda standardsystemkonverteraren i en anpassad konverterare. Det gör du genom att hämta systemkonverteraren från JsonSerializerOptions.Default egenskapen, som du ser i följande exempel:
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);
}
}
Hantera nullvärden
Som standard hanterar serialiseraren null-värden på följande sätt:
För referenstyper och Nullable<T> typer:
- Den skickas
null
inte till anpassade konverterare vid serialisering. - Den skickas
JsonTokenType.Null
inte till anpassade konverterare vid deserialisering. - Den returnerar en
null
instans vid deserialisering. - Den skriver
null
direkt med skrivaren om serialisering.
- Den skickas
För icke-nullbara värdetyper:
- Den skickas
JsonTokenType.Null
till anpassade konverterare vid deserialisering. (Om ingen anpassad konverterare är tillgänglig genereras ettJsonException
undantag av den interna konverteraren för typen.)
- Den skickas
Det här null-hanteringsbeteendet är främst för att optimera prestanda genom att hoppa över ett extra anrop till konverteraren. Dessutom undviker den att tvinga konverterare för null-typer att söka efter null
i början av varje Read
och Write
metod åsidosättning.
Om du vill aktivera en anpassad konverterare att hantera null
för en referens- eller värdetyp åsidosätter du JsonConverter<T>.HandleNull för att returnera true
, som du ser i följande exempel:
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.
Bevara referenser
Som standard cachelagras endast referensdata för varje anrop till Serialize eller Deserialize. Om du vill spara referenser från ett Serialize
/Deserialize
anrop till ett annat rotar du instansen ReferenceResolver på anropsplatsen Serialize
/Deserialize
för . Följande kod visar ett exempel för det här scenariot:
- Du skriver en anpassad konverterare för
Company
typen. - Du vill inte serialisera
Supervisor
egenskapen manuellt, som är enEmployee
. Du vill delegera det till serialiseraren och du vill också bevara de referenser som du redan har sparat.
Här är klasserna Employee
och 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; }
}
Konverteraren ser ut så här:
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();
}
}
En klass som härleds från ReferenceResolver lagrar referenserna i en ordlista:
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;
}
}
En klass som härleds från ReferenceHandler innehåller en instans av MyReferenceResolver
och skapar endast en ny instans när det behövs (i en metod med namnet Reset
i det här exemplet):
class MyReferenceHandler : ReferenceHandler
{
public MyReferenceHandler() => Reset();
private ReferenceResolver? _rootedResolver;
public override ReferenceResolver CreateResolver() => _rootedResolver!;
public void Reset() => _rootedResolver = new MyReferenceResolver();
}
När exempelkoden anropar serialiseraren använder den ReferenceHandler en JsonSerializerOptions instans där egenskapen är inställd på en instans av MyReferenceHandler
. När du följer det här mönstret måste du återställa ReferenceResolver
ordlistan när du är klar med serialiseringen, så att den inte växer för alltid.
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();
Föregående exempel utför endast serialisering, men en liknande metod kan användas för deserialisering.
Andra anpassade konverterarexempel
Artikeln Migrera från Newtonsoft.Json till System.Text.Json innehåller ytterligare exempel på anpassade konverterare.
Mappen enhetstest i källkoden System.Text.Json.Serialization
innehåller andra anpassade konverterarexempel, till exempel:
- Int32-konverterare som konverterar null till 0 vid deserialisering
- Int32-konverterare som tillåter både sträng- och talvärden vid deserialisering
- Uppräkningskonverterare
- Lista<T-konverterare> som accepterar externa data
- Long[] konverterare som fungerar med en kommaavgränsad lista med tal
Om du behöver göra en konverterare som ändrar beteendet för en befintlig inbyggd konverterare kan du hämta källkoden för den befintliga konverteraren så att den fungerar som en startpunkt för anpassning.