Jak pisać niestandardowe konwertery na potrzeby serializacji JSON (marshalling) na platformie .NET
W tym artykule pokazano, jak utworzyć niestandardowe konwertery dla klas serializacji JSON, które znajdują się w System.Text.Json przestrzeni nazw. Aby zapoznać się z wprowadzeniem do System.Text.Json
programu , zobacz Jak serializować i deserializować dane JSON na platformie .NET.
Konwerter to klasa, która konwertuje obiekt lub wartość na i z formatu JSON.
System.Text.Json
Przestrzeń nazw ma wbudowane konwertery dla większości typów pierwotnych mapowanych na typy pierwotne Języka JavaScript. Możesz napisać niestandardowe konwertery, aby zastąpić domyślne zachowanie wbudowanego konwertera. Na przykład:
- Wartości mogą
DateTime
być reprezentowane przez format mm/dd/rrrr. Domyślnie obsługiwany jest profil ISO 8601-1:2019, w tym profil RFC 3339. Aby uzyskać więcej informacji, zobacz Obsługa funkcji DateTime i DateTimeOffset w systemie System.Text.Json. - Możesz na przykład serializować kod POCO jako ciąg JSON, na przykład z typem
PhoneNumber
.
Możesz również napisać konwertery niestandardowe, aby dostosować lub rozszerzyć System.Text.Json
o nowe funkcje. W dalszej części tego artykułu opisano następujące scenariusze:
- Deserializowanie wywnioskowanych typów we właściwościach obiektów.
- Obsługa deserializacji polimorficznej.
-
Obsługa rundy dla
Stack
typów. - Użyj domyślnego konwertera systemu.
Nie można użyć języka Visual Basic do pisania konwerterów niestandardowych, ale może wywoływać konwertery implementowane w bibliotekach języka C#. Aby uzyskać więcej informacji, zobacz Obsługa języka Visual Basic.
Niestandardowe wzorce konwerterów
Istnieją dwa wzorce tworzenia konwertera niestandardowego: podstawowy wzorzec i wzorzec fabryki. Wzorzec fabryki jest przeznaczony dla konwerterów, które obsługują typ Enum
lub otwarte typy ogólne. Podstawowy wzorzec dotyczy typów niegenerycznych i zamkniętych typów ogólnych. Na przykład konwertery dla następujących typów wymagają wzorca fabryki:
Oto kilka przykładów typów, które mogą być obsługiwane przez podstawowy wzorzec:
Podstawowy wzorzec tworzy klasę, która może obsługiwać jeden typ. Wzorzec fabryki tworzy klasę, która określa, w czasie wykonywania, który określony typ jest wymagany i dynamicznie tworzy odpowiedni konwerter.
Przykładowy konwerter podstawowy
Poniższy przykład to konwerter, który zastępuje domyślną serializacji dla istniejącego typu danych. Konwerter używa formatu mm/dd/rrrr dla DateTimeOffset
właściwości.
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));
}
}
Przykładowy konwerter wzorców fabryki
Poniższy kod przedstawia niestandardowy konwerter, który działa z Dictionary<Enum,TValue>
programem . Kod jest zgodny ze wzorcem fabryki, ponieważ pierwszy parametr typu ogólnego to Enum
, a drugi jest otwarty. Metoda CanConvert
zwraca tylko dla true
elementu z dwoma parametrami ogólnymi, z których pierwszy jest typem Dictionary
Enum
. Konwerter wewnętrzny pobiera istniejący konwerter do obsługi niezależnie od typu udostępnianego w czasie wykonywania dla TValue
programu .
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();
}
}
}
}
Kroki, które należy wykonać zgodnie ze wzorcem podstawowym
W poniższych krokach wyjaśniono, jak utworzyć konwerter, postępując zgodnie z podstawowym wzorcem:
- Utwórz klasę, która pochodzi z JsonConverter<T> tego, gdzie
T
jest typem, który ma być serializowany i deserializowany. - Zastąpij metodę
Read
deserializacji przychodzącego kodu JSON i przekonwertuj ją na typT
. Utf8JsonReader Użyj metody przekazanej do metody , aby odczytać kod JSON. Nie musisz martwić się o obsługę częściowych danych, ponieważ serializator przekazuje wszystkie dane dla bieżącego zakresu JSON. Dlatego nie jest konieczne wywołanie Skip ani TrySkip sprawdzenie, czy zwraca wartość Readtrue
. - Zastąpij metodę
Write
, aby serializować przychodzący obiekt typuT
. Użyj metody przekazanej Utf8JsonWriter do metody , aby zapisać kod JSON. - Zastąpij metodę
CanConvert
tylko w razie potrzeby. Domyślna implementacja zwracatrue
wartość, gdy typ do konwersji ma typT
. W związku z tym konwertery obsługujące tylko typT
nie muszą zastępować tej metody. Aby zapoznać się z przykładem konwertera, który musi zastąpić tę metodę, zobacz sekcję deserializacji polimorficznej w dalszej części tego artykułu.
Możesz odwołać się do wbudowanego kodu źródłowego konwerterów jako implementacji referencyjnych do pisania konwerterów niestandardowych.
Kroki, które należy wykonać zgodnie ze wzorcem fabryki
W poniższych krokach wyjaśniono, jak utworzyć konwerter, postępując zgodnie ze wzorcem fabryki:
- Utwórz klasę pochodzącą z klasy JsonConverterFactory.
- Zastąpij metodę zwracaną
CanConvert
true
, gdy typ do konwersji jest taki, który konwerter może obsłużyć. Jeśli na przykład konwerter jest przeznaczony dlaList<T>
elementu , może obsługiwać tylko elementyList<int>
,List<string>
iList<DateTime>
. - Zastąpi metodę
CreateConverter
, aby zwrócić wystąpienie klasy konwertera, które będzie obsługiwać konwersję typu na konwersję podaną w czasie wykonywania. - Utwórz klasę konwertera utworzoną przez metodę
CreateConverter
.
Wzorzec fabryki jest wymagany dla otwartych typów ogólnych, ponieważ kod do konwersji obiektu na i z ciągu nie jest taki sam dla wszystkich typów. Konwerter otwartego typu ogólnego (List<T>
na przykład) musi utworzyć konwerter dla zamkniętego typu ogólnego (List<DateTime>
na przykład) za kulisami. Kod musi być napisany w celu obsługi każdego typu zamkniętego ogólnego, który może obsłużyć konwerter.
Typ Enum
jest podobny do otwartego typu ogólnego: konwerter musi Enum
utworzyć konwerter dla określonego Enum
(WeekdaysEnum
na przykład) za kulisami.
Użycie Utf8JsonReader
metody w metodzie Read
Jeśli konwerter konwertuje obiekt JSON, Utf8JsonReader
obiekt zostanie umieszczony na początkowym tokenie obiektu po rozpoczęciu Read
metody. Następnie należy odczytać wszystkie tokeny w tym obiekcie i zamknąć metodę z czytnikiem umieszczonym na odpowiednim tokenie obiektu końcowego. Jeśli odczytasz poza końcem obiektu lub zatrzymasz się przed osiągnięciem JsonException
odpowiedniego tokenu końcowego, otrzymasz wyjątek wskazujący, że:
Konwerter "ConverterName" odczytuje za dużo lub za mało.
Przykład można znaleźć w powyższym konwerterze przykładów wzorca fabryki. Metoda Read
rozpoczyna się od sprawdzenia, czy czytnik jest umieszczony na tokenie obiektu startowego. Odczytuje do momentu znalezienia, że jest on umieszczony w następnym tokenie obiektu końcowego. Zatrzymuje się on na następnym tokenie obiektu końcowego, ponieważ nie ma żadnych pośredniczące tokenów obiektów początkowych, które wskazują obiekt w obiekcie. Ta sama reguła dotycząca tokenu rozpoczęcia i tokenu końcowego ma zastosowanie w przypadku konwertowania tablicy. Aby zapoznać się z przykładem, zobacz Stack<T>
przykładowy konwerter w dalszej części tego artykułu.
Obsługa błędów
Serializator zapewnia specjalną obsługę typów wyjątków JsonException i NotSupportedException.
Wyjątek JsonException
W przypadku zgłoszenia JsonException
komunikatu bez serializator tworzy komunikat zawierający ścieżkę do części kodu JSON, która spowodowała błąd. Na przykład instrukcja throw new JsonException()
generuje komunikat o błędzie podobny do następującego przykładu:
Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.
Jeśli podasz komunikat (na przykład throw new JsonException("Error occurred")
), serializator nadal ustawia Pathwłaściwości , LineNumberi BytePositionInLine .
NotSupportedException
Jeśli zgłosisz element NotSupportedException
, zawsze otrzymasz informacje o ścieżce w komunikacie. Jeśli podasz komunikat, informacje o ścieżce są do niego dołączane. Na przykład instrukcja throw new NotSupportedException("Error occurred.")
generuje komunikat o błędzie podobny do następującego przykładu:
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
Kiedy należy zgłosić typ wyjątku
Gdy ładunek JSON zawiera tokeny, które nie są prawidłowe dla typu deserializacji, należy zgłosić wartość JsonException
.
Jeśli chcesz nie zezwalać na niektóre typy, wyrzuć wartość NotSupportedException
. Ten wyjątek polega na tym, że serializator automatycznie zgłasza typy, które nie są obsługiwane. Na przykład System.Type
nie jest obsługiwana ze względów bezpieczeństwa, dlatego próba deserializacji powoduje, że element NotSupportedException
.
W razie potrzeby można zgłaszać inne wyjątki, ale nie zawierają one automatycznie informacji o ścieżce JSON.
Rejestrowanie konwertera niestandardowego
Zarejestruj konwerter niestandardowy, aby używać Serialize
tych metod i Deserialize
. Wybierz jedną z następujących metod:
- Dodaj wystąpienie klasy konwertera do kolekcji JsonSerializerOptions.Converters .
- Zastosuj atrybut [JsonConverter] do właściwości, które wymagają konwertera niestandardowego.
- Zastosuj atrybut [JsonConverter] do klasy lub struktury reprezentującej niestandardowy typ wartości.
Przykład rejestracji — kolekcja konwerterów
Oto przykład, który sprawia, że właściwość DateTimeOffsetJsonConverter jest domyślna dla właściwości typu DateTimeOffset:
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true
};
serializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);
Załóżmy, że serializujesz wystąpienie następującego typu:
public class WeatherForecast
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Oto przykład danych wyjściowych JSON pokazujących, że użyto konwertera niestandardowego:
{
"Date": "08/01/2019",
"TemperatureCelsius": 25,
"Summary": "Hot"
}
Poniższy kod używa tego samego podejścia do deserializacji przy użyciu konwertera niestandardowego DateTimeOffset
:
var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions)!;
Przykład rejestracji — [JsonConverter] we właściwości
Poniższy kod wybiera niestandardowy konwerter dla Date
właściwości :
public class WeatherForecastWithConverterAttribute
{
[JsonConverter(typeof(DateTimeOffsetJsonConverter))]
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Kod do serializacji WeatherForecastWithConverterAttribute
nie wymaga użycia elementu JsonSerializeOptions.Converters
:
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);
Kod do deserializacji również nie wymaga użycia elementu Converters
:
weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithConverterAttribute>(jsonString)!;
Przykład rejestracji — [JsonConverter] w typie
Oto kod, który tworzy strukturę i stosuje [JsonConverter]
do niego atrybut:
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);
}
}
}
Oto konwerter niestandardowy dla poprzedniej struktury:
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());
}
}
Atrybut [JsonConverter]
w strukturę rejestruje konwerter niestandardowy jako domyślny dla właściwości typu Temperature
. Konwerter jest automatycznie używany we TemperatureCelsius
właściwości następującego typu podczas serializacji lub deserializacji:
public class WeatherForecastWithTemperatureStruct
{
public DateTimeOffset Date { get; set; }
public Temperature TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
Pierwszeństwo rejestracji konwertera
Podczas serializacji lub deserializacji konwerter jest wybierany dla każdego elementu JSON w następującej kolejności, wymienione z najwyższego priorytetu do najniższego:
-
[JsonConverter]
zastosowane do właściwości. - Konwerter dodany do kolekcji
Converters
. -
[JsonConverter]
zastosowane do niestandardowego typu wartości lub poCO.
Jeśli w Converters
kolekcji zarejestrowano wiele konwerterów niestandardowych dla typu, używany jest pierwszy konwerter, który zwraca true
wartość .CanConvert
Wbudowany konwerter jest wybierany tylko wtedy, gdy nie zarejestrowano żadnego odpowiedniego konwertera niestandardowego.
Przykłady konwerterów dla typowych scenariuszy
W poniższych sekcjach przedstawiono przykłady konwerterów, które dotyczą niektórych typowych scenariuszy, które wbudowane funkcje nie obsługują.
- Deserializowanie wywnioskowanych typów we właściwościach obiektów.
-
Obsługa rundy dla
Stack
typów. - Użyj domyślnego konwertera systemu.
Aby zapoznać się z przykładowym konwerterem DataTable, zobacz Obsługiwane typy.
Deserializowanie wywnioskowanych typów we właściwościach obiektu
Podczas deserializacji do właściwości typu object
JsonElement
tworzony jest obiekt. Przyczyną jest to, że deserializator nie wie, jaki typ CLR utworzyć, i nie próbuje odgadnąć. Jeśli na przykład właściwość JSON ma wartość "true", deserializator nie wywnioskuje, że wartość jest wartością Boolean
, a jeśli element ma wartość "01/01/2019", deserializator nie wywnioskuje, że jest to DateTime
.
Wnioskowanie typu może być niedokładne. Jeśli deserializator analizuje liczbę JSON, która nie ma punktu dziesiętnego jako long
, może to spowodować problemy poza zakresem, jeśli wartość została pierwotnie serializowana jako ulong
lub BigInteger
. Analizowanie liczby, która ma punkt dziesiętny, ponieważ double
może stracić precyzję, jeśli liczba została pierwotnie serializowana jako decimal
.
W przypadku scenariuszy wymagających wnioskowania typu poniższy kod przedstawia niestandardowy konwerter właściwości object
. Kod konwertuje:
-
true
ifalse
doBoolean
- Liczby bez liczby dziesiętnej do
long
- Liczby z wartością dziesiętną do
double
- Daty do
DateTime
- Ciągi do
string
- Wszystko inne do
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"
//}
W przykładzie pokazano kod konwertera i klasę WeatherForecast
z właściwościami object
. Metoda Main
deserializuje ciąg JSON w WeatherForecast
wystąpieniu, najpierw bez użycia konwertera, a następnie przy użyciu konwertera. Dane wyjściowe konsoli pokazują, że bez konwertera typ czasu wykonywania dla Date
właściwości to JsonElement
; z konwerterem typ czasu wykonywania to DateTime
.
Folder testów jednostkowych w System.Text.Json.Serialization
przestrzeni nazw zawiera więcej przykładów konwerterów niestandardowych, które obsługują deserializacji do object
właściwości.
Obsługa deserializacji polimorficznej
Platforma .NET 7 zapewnia obsługę zarówno serializacji polimorficznej, jak i deserializacji. Jednak w poprzednich wersjach platformy .NET istniała ograniczona obsługa serializacji polimorficznej i brak obsługi deserializacji. Jeśli używasz platformy .NET 6 lub starszej wersji, deserializacja wymaga niestandardowego konwertera.
Załóżmy na przykład, że masz abstrakcyjną klasę bazową Person
z klasami pochodnymi Employee
i .Customer
Deserializacji polimorficznej oznacza, że w czasie projektowania można określić Person
jako cel deserializacji, a Customer
Employee
obiekty w formacie JSON są poprawnie deserializowane w czasie wykonywania. Podczas deserializacji należy znaleźć wskazówki identyfikujące wymagany typ w formacie JSON. Rodzaje dostępnych wskazówek różnią się w zależności od scenariusza. Na przykład właściwość dyskryminująca może być dostępna lub może być konieczne poleganie na obecności lub braku określonej właściwości. Bieżąca wersja System.Text.Json
programu nie udostępnia atrybutów w celu określenia sposobu obsługi scenariuszy deserializacji polimorficznej, dlatego wymagane są niestandardowe konwertery.
Poniższy kod przedstawia klasę bazową, dwie klasy pochodne i niestandardowy konwerter dla nich. Konwerter używa właściwości dyskryminującej do deserializacji polimorficznej. Dyskryminujący typ nie znajduje się w definicjach klas, ale jest tworzony podczas serializacji i jest odczytywany podczas deserializacji.
Ważne
Przykładowy kod wymaga, aby pary nazw/wartości obiektów JSON pozostawały w porządku, co nie jest standardowym wymaganiem w formacie 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();
}
}
}
Poniższy kod rejestruje konwerter:
var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new PersonConverterWithTypeDiscriminator());
Konwerter może deserializować kod JSON, który został utworzony przy użyciu tego samego konwertera do serializacji, na przykład:
[
{
"TypeDiscriminator": 1,
"CreditLimit": 10000,
"Name": "John"
},
{
"TypeDiscriminator": 2,
"OfficeNumber": "555-1234",
"Name": "Nancy"
}
]
Kod konwertera w poprzednim przykładzie odczytuje i zapisuje każdą właściwość ręcznie. Alternatywą jest wywołanie Deserialize
lub Serialize
zrobienie niektórych prac. Aby zapoznać się z przykładem, zobacz ten wpis StackOverflow.
Alternatywny sposób deserializacji polimorficznej
Możesz wywołać Deserialize
metodę Read
:
- Utwórz klon
Utf8JsonReader
wystąpienia. PonieważUtf8JsonReader
jest to struktura, wymaga to tylko instrukcji przypisania. - Użyj klonu, aby odczytać tokeny dyskryminujące.
- Wywołaj
Deserialize
metodę przy użyciu oryginalnegoReader
wystąpienia, gdy znasz potrzebny typ. Można wywołać metodęDeserialize
, ponieważ oryginalneReader
wystąpienie jest nadal umieszczone w celu odczytania tokenu obiektu początkowego.
Wadą tej metody jest to, że nie można przekazać oryginalnego wystąpienia opcji, które rejestruje konwerter na Deserialize
. Spowoduje to przepełnienie stosu, jak wyjaśniono we właściwościach Wymagane. W poniższym przykładzie przedstawiono metodę Read
, która korzysta z tej alternatywy:
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;
}
Obsługa rundy dla Stack
typów
Jeśli deserializujesz ciąg JSON do Stack
obiektu, a następnie serializujesz ten obiekt, zawartość stosu jest w odwrotnej kolejności. To zachowanie dotyczy następujących typów i interfejsów oraz typów zdefiniowanych przez użytkownika, które pochodzą z nich:
Aby zapewnić obsługę serializacji i deserializacji, która zachowuje oryginalną kolejność w stosie, wymagany jest konwerter niestandardowy.
Poniższy kod przedstawia niestandardowy konwerter, który umożliwia zaokrąglanie do i z Stack<T>
obiektów:
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();
}
}
}
Poniższy kod rejestruje konwerter:
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonConverterFactoryForStackOfT());
Użyj domyślnego konwertera systemu
W niektórych scenariuszach możesz chcieć użyć domyślnego konwertera systemu w konwerterze niestandardowym. W tym celu pobierz konwerter systemu z JsonSerializerOptions.Default właściwości , jak pokazano w poniższym przykładzie:
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);
}
}
Obsługa wartości null
Domyślnie serializator obsługuje wartości null w następujący sposób:
W przypadku typów i Nullable<T> typów referencyjnych:
- Nie jest przekazywany
null
do konwerterów niestandardowych w przypadku serializacji. - Nie jest przekazywany
JsonTokenType.Null
do konwerterów niestandardowych w przypadku deserializacji. - Zwraca
null
wystąpienie deserializacji. - Pisze
null
bezpośrednio z zapisem w sprawie serializacji.
- Nie jest przekazywany
W przypadku typów wartości innych niż null:
-
JsonTokenType.Null
Przekazuje on do niestandardowych konwerterów w przypadku deserializacji. (Jeśli nie ma dostępnego konwertera niestandardowego,JsonException
wyjątek jest zgłaszany przez wewnętrzny konwerter dla typu).
-
To zachowanie obsługi wartości null polega przede wszystkim na optymalizacji wydajności przez pominięcie dodatkowego wywołania konwertera. Ponadto unika wymuszania przesłonięcia konwerterów dla typów null
dopuszczanych do wartości null na początku każdej Read
metody i Write
.
Aby włączyć konwerter niestandardowy do obsługi null
dla typu odwołania lub wartości, zastąpić JsonConverter<T>.HandleNull , aby zwrócić true
wartość , jak pokazano w poniższym przykładzie:
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.
Zachowywanie odwołań
Domyślnie dane referencyjne są buforowane tylko dla każdego wywołania metody Serialize lub Deserialize. Aby utrwalać odwołania z jednego Serialize
/Deserialize
wywołania do innego, root ReferenceResolver wystąpienia w lokacji Serialize
/Deserialize
wywołania klasy . Poniższy kod przedstawia przykład dla tego scenariusza:
- Dla typu należy napisać konwerter
Company
niestandardowy. - Nie chcesz ręcznie serializować
Supervisor
właściwości , czyliEmployee
. Chcesz delegować je do serializatora, a także chcesz zachować zapisane odwołania.
Employee
Oto klasy iCompany
:
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; }
}
Konwerter wygląda następująco:
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();
}
}
Klasa, która pochodzi z ReferenceResolver magazynów odwołań w słowniku:
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;
}
}
Klasa, która pochodzi z ReferenceHandlerMyReferenceResolver
wystąpienia klasy i tworzy nowe wystąpienie tylko wtedy, gdy jest to konieczne (w metodzie o nazwie Reset
w tym przykładzie):
class MyReferenceHandler : ReferenceHandler
{
public MyReferenceHandler() => Reset();
private ReferenceResolver? _rootedResolver;
public override ReferenceResolver CreateResolver() => _rootedResolver!;
public void Reset() => _rootedResolver = new MyReferenceResolver();
}
Gdy przykładowy kod wywołuje serializator, używa JsonSerializerOptions wystąpienia, w którym ReferenceHandler właściwość jest ustawiona na wystąpienie MyReferenceHandler
klasy . Po przestrzeganiu tego wzorca pamiętaj, aby zresetować słownik po zakończeniu ReferenceResolver
serializacji, aby zachować jego rozwój na zawsze.
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();
Powyższy przykład dotyczy tylko serializacji, ale podobne podejście można zastosować do deserializacji.
Inne niestandardowe przykłady konwerterów
Artykuł Migrowanie z Newtonsoft.Json do System.Text.Json zawiera dodatkowe przykłady konwerterów niestandardowych.
Folder testów jednostkowych w kodzie źródłowym System.Text.Json.Serialization
zawiera inne niestandardowe przykłady konwerterów, takie jak:
- Konwerter int32, który konwertuje wartość null na wartość 0 na deserializacji
- Konwerter Int32, który umożliwia deserializacji wartości ciągów i liczb
- Konwerter wyliczenia
- Konwerter T< listy>, który akceptuje dane zewnętrzne
- Konwerter Long[] współdziałający z rozdzielaną przecinkami listą liczb
Jeśli musisz utworzyć konwerter, który modyfikuje zachowanie istniejącego wbudowanego konwertera, możesz uzyskać kod źródłowy istniejącego konwertera , aby służyć jako punkt wyjścia do dostosowywania.