Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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.Jsonprogramu , 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. Przykład:
- Wartości mogą
DateTimebyć 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
Stacktypó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 w czasie wykonywania określa, jaki konkretny typ jest potrzebny 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 DictionaryEnum. Wewnętrzny konwerter pobiera istniejący konwerter do obsługi dowolnego typu dostarczanego w czasie wykonywania dla 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();
}
}
}
}
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
Tjest typem, który ma być serializowany i deserializowany. - Zastąpij metodę
Readdeserializacji 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ę
CanConverttylko w razie potrzeby. Domyślna implementacja zwracatruewartość, gdy typ do konwersji ma typT. W związku z tym konwertery obsługujące tylko typTnie 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ą
CanConverttrue, 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>. - Nadpisz metodę
CreateConverter, aby zwrócić instancję klasy konwertera, który będzie obsługiwał typ do konwersji, dostarczany 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 (WeekdaysEnumna przykład) za kulisami.
Użycie Utf8JsonReader metody w metodzie Read
Jeśli konwerter konwertuje obiekt JSON, Utf8JsonReader jest ustawiony na początku tokenu obiektu, gdy rozpoczyna się metoda Read. 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.
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ługiwany ze względów bezpieczeństwa, więc próba deserializacji powoduje 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
Stacktypó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 objectJsonElement 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:
-
trueifalsedoBoolean - 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)
{
var runtimeType = objectToWrite.GetType();
if (runtimeType == typeof(object))
{
writer.WriteStartObject();
writer.WriteEndObject();
return;
}
JsonSerializer.Serialize(writer, objectToWrite, runtimeType, 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 Run()
{
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 środowiska uruchomieniowego dla Date właściwości to JsonElement; z konwerterem typ środowiska uruchomieniowego 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 Deserializacja polimorficzna oznacza, że w czasie projektowania można określić Person jako docelową deserializację, a obiekty Customer i Employee 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.
Important
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
Utf8JsonReaderwystąpienia. PonieważUtf8JsonReaderjest to struktura, wymaga to tylko instrukcji przypisania. - Użyj klonu, aby odczytać tokeny dyskryminujące.
- Wywołaj
Deserializemetodę przy użyciu oryginalnegoReaderwystąpienia, gdy znasz potrzebny typ. Można wywołać metodęDeserialize, ponieważ oryginalneReaderwystą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 przekazuje
nulldo konwerterów niestandardowych podczas serializacji. - Nie przekazuje
JsonTokenType.Nulldo konwerterów niestandardowych podczas deserializacji. - Zwraca
nullwystąpienie deserializacji. - Pisze
nullbezpośrednio z zapisem w sprawie serializacji.
- Nie przekazuje
W przypadku typów wartości innych niż null:
-
JsonTokenType.NullPrzekazuje on do niestandardowych konwerterów w przypadku deserializacji. (Jeśli nie ma dostępnego konwertera niestandardowego,JsonExceptionwyją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ć truewartość , 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 Run()
{
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/Deserializewywołania klasy . Poniższy kod przedstawia przykład dla tego scenariusza:
- Dla typu należy napisać konwerter
Companyniestandardowy. - Nie chcesz ręcznie serializować
Supervisorwł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 MyReferenceHandlerklasy . 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.
Ograniczenia ReferenceResolver związane z konwerterami niestandardowymi
Jeśli używasz Preserve, należy pamiętać, że stan obsługi referencji nie jest zachowywany, gdy serializator wywołuje konwerter niestandardowy. Oznacza to, że jeśli masz niestandardowy konwerter dla typu, który jest częścią grafu obiektu serializowanego lub deserializowanego z włączonym zachowaniem odwołania, konwerter i wszystkie zagnieżdżone wywołania serializacji nie będą miały dostępu do bieżącej instancji ReferenceResolver.
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 enum
- 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.