Jak pisać konwertery niestandardowe na potrzeby serializacji JSON (kierowanie) w programie .NETHow to write custom converters for JSON serialization (marshalling) in .NET

W tym artykule pokazano, jak utworzyć niestandardowe konwertery dla klas serializacji JSON, które są dostępne w przestrzeni nazw <xref:[!OP.NO-LOC(System.Text.Json)]>.This article shows how to create custom converters for the JSON serialization classes that are provided in the <xref:[!OP.NO-LOC(System.Text.Json)]> namespace. Aby zapoznać się z wprowadzeniem do [!OP.NO-LOC(System.Text.Json)], zobacz jak serializować i deserializować kod JSON w programie .NET.For an introduction to [!OP.NO-LOC(System.Text.Json)], see How to serialize and deserialize JSON in .NET.

Konwerter jest klasą, która konwertuje obiekt lub wartość do i z formatu JSON.A converter is a class that converts an object or a value to and from JSON. Przestrzeń nazw [!OP.NO-LOC(System.Text.Json)] zawiera wbudowane konwertery dla większości typów pierwotnych, które są mapowane na elementy pierwotne języka JavaScript.The [!OP.NO-LOC(System.Text.Json)] namespace has built-in converters for most primitive types that map to JavaScript primitives. Możesz pisać niestandardowe konwertery:You can write custom converters:

  • Aby zastąpić domyślne zachowanie konwertera wbudowanego.To override the default behavior of a built-in converter. Na przykład możesz chcieć, aby wartości DateTime były reprezentowane w formacie mm/dd/rrrr zamiast domyślnego formatu ISO 8601-1:2019.For example, you might want DateTime values to be represented by mm/dd/yyyy format instead of the default ISO 8601-1:2019 format.
  • Do obsługi niestandardowego typu wartości.To support a custom value type. Na przykład struktura PhoneNumber.For example, a PhoneNumber struct.

Możesz również napisać niestandardowe konwertery, aby dostosować lub zwiększyć [!OP.NO-LOC(System.Text.Json)] z funkcjami niezawartymi w bieżącej wersji.You can also write custom converters to customize or extend [!OP.NO-LOC(System.Text.Json)] with functionality not included in the current release. Poniższe scenariusze zostały omówione w dalszej części tego artykułu:The following scenarios are covered later in this article:

Wzorce niestandardowego konwerteraCustom converter patterns

Istnieją dwa wzorce do tworzenia niestandardowego konwertera: wzorzec podstawowy i wzorzec fabryki.There are two patterns for creating a custom converter: the basic pattern and the factory pattern. Wzorzec fabryki jest przeznaczony dla konwerterów, które obsługują typ Enum lub otwierają typy ogólne.The factory pattern is for converters that handle type Enum or open generics. Wzorzec podstawowy dotyczy typów ogólnych nieogólnych i zamkniętych.The basic pattern is for non-generic and closed generic types. Na przykład konwertery dla następujących typów wymagają wzorca fabryki:For example, converters for the following types require the factory pattern:

  • Dictionary<TKey, TValue>
  • Enum
  • List<T>

Niektóre przykłady typów, które mogą być obsługiwane przez wzorzec Basic, obejmują:Some examples of types that can be handled by the basic pattern include:

  • Dictionary<int, string>
  • WeekdaysEnum
  • List<DateTimeOffset>
  • DateTime
  • Int32

Wzorzec podstawowy tworzy klasę, która może obsługiwać jeden typ.The basic pattern creates a class that can handle one type. Wzorzec fabryki tworzy klasę, która określa w czasie wykonywania, który określony typ jest wymagany i dynamicznie tworzy odpowiedni konwerter.The factory pattern creates a class that determines at runtime which specific type is required and dynamically creates the appropriate converter.

Przykładowy konwerter podstawowySample basic converter

Poniższy przykład to konwerter, który zastąpi domyślną serializację dla istniejącego typu danych.The following sample is a converter that overrides default serialization for an existing data type. Konwerter używa formatu mm/dd/rrrr do DateTimeOffset właściwości.The converter uses mm/dd/yyyy format for DateTimeOffset properties.

using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DateTimeOffsetConverter : 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 fabrykiSample factory pattern converter

Poniższy kod przedstawia niestandardowy konwerter, który działa z Dictionary<Enum,TValue>.The following code shows a custom converter that works with Dictionary<Enum,TValue>. Kod jest zgodny ze wzorcem fabryki, ponieważ pierwszy parametr typu ogólnego to Enum, a drugi jest otwarty.The code follows the factory pattern because the first generic type parameter is Enum and the second is open. Metoda CanConvert zwraca true tylko dla Dictionary z dwoma parametrami ogólnymi, z których pierwszy jest typem Enum.The CanConvert method returns true only for a Dictionary with two generic parameters, the first of which is an Enum type. Wewnętrzny konwerter pobiera istniejący konwerter do obsługi dowolnego typu w czasie wykonywania dla TValue.The inner converter gets an existing converter to handle whichever type is provided at runtime for TValue.

using System;
using System.Collections.Generic;
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 keyType = type.GetGenericArguments()[0];
            Type valueType = type.GetGenericArguments()[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    new Type[] { keyType, valueType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: new object[] { options },
                culture: null);

            return converter;
        }

        private class DictionaryEnumConverterInner<TKey, TValue> : 
            JsonConverter<Dictionary<TKey, TValue>> where TKey : struct, Enum
        {
            private readonly JsonConverter<TValue> _valueConverter;
            private Type _keyType;
            private Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter if available.
                _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();
                }

                Dictionary<TKey, TValue> 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.
                    TValue v;
                    if (_valueConverter != null)
                    {
                        reader.Read();
                        v = _valueConverter.Read(ref reader, _valueType, options);
                    }
                    else
                    {
                        v = JsonSerializer.Deserialize<TValue>(ref reader, options);
                    }

                    // Add to dictionary.
                    dictionary.Add(key, v);
                }

                throw new JsonException();
            }

            public override void Write(
                Utf8JsonWriter writer, 
                Dictionary<TKey, TValue> dictionary, 
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();

                foreach (KeyValuePair<TKey, TValue> kvp in dictionary)
                {
                    writer.WritePropertyName(kvp.Key.ToString());

                    if (_valueConverter != null)
                    {
                        _valueConverter.Write(writer, kvp.Value, options);
                    }
                    else
                    {
                        JsonSerializer.Serialize(writer, kvp.Value, options);
                    }
                }

                writer.WriteEndObject();
            }
        }
    }
}

Poprzedni kod jest taki sam jak w słowniku pomocy technicznej z kluczem innym niż ciąg w dalszej części tego artykułu.The preceding code is the same as what is shown in the Support Dictionary with non-string key later in this article.

Kroki prowadzące do wzorca podstawowegoSteps to follow the basic pattern

Poniższe kroki wyjaśniają, jak utworzyć konwerter, wykonując następujące czynności:The following steps explain how to create a converter by following the basic pattern:

  • Utwórz klasę, która pochodzi od <xref:[!OP.NO-LOC(System.Text.Json)].Serialization.JsonConverter%601>, gdzie T jest typem, który ma być serializowany i deserializowany.Create a class that derives from <xref:[!OP.NO-LOC(System.Text.Json)].Serialization.JsonConverter%601> where T is the type to be serialized and deserialized.
  • Zastąp metodę Read, aby deserializować przychodzącego pliku JSON i przekonwertować go na Ttypu.Override the Read method to deserialize the incoming JSON and convert it to type T. Użyj <xref:[!OP.NO-LOC(System.Text.Json)].Utf8JsonReader>, który jest przesyłany do metody, aby odczytać kod JSON.Use the <xref:[!OP.NO-LOC(System.Text.Json)].Utf8JsonReader> that is passed to the method to read the JSON.
  • Zastąp metodę Write, aby serializować obiekt przychodzący typu T.Override the Write method to serialize the incoming object of type T. Użyj <xref:[!OP.NO-LOC(System.Text.Json)].Utf8JsonWriter>, który jest przesyłany do metody, aby zapisać kod JSON.Use the <xref:[!OP.NO-LOC(System.Text.Json)].Utf8JsonWriter> that is passed to the method to write the JSON.
  • Zastąp metodę CanConvert tylko w razie potrzeby.Override the CanConvert method only if necessary. Domyślna implementacja zwraca true, gdy typ do przekonwertowania jest typu T.The default implementation returns true when the type to convert is of type T. W związku z tym konwertery obsługujące tylko typ T nie muszą przesłaniać tej metody.Therefore, converters that support only type T don't need to override this method. Aby zapoznać się z przykładem konwertera, który musi przesłonić tę metodę, zobacz sekcję deserializacji polimorficzną w dalszej części tego artykułu.For an example of a converter that does need to override this method, see the polymorphic deserialization section later in this article.

Możesz odwołać się do wbudowanego kodu źródłowego konwerterów jako implementacji odwołań do pisania konwerterów niestandardowych.You can refer to the built-in converters source code as reference implementations for writing custom converters.

Kroki prowadzące do wzorca fabrykiSteps to follow the factory pattern

Poniższe kroki wyjaśniają, jak utworzyć konwerter, postępując zgodnie z wzorcem fabryki:The following steps explain how to create a converter by following the factory pattern:

  • Utwórz klasę pochodzącą z <xref:[!OP.NO-LOC(System.Text.Json)].Serialization.JsonConverterFactory>.Create a class that derives from <xref:[!OP.NO-LOC(System.Text.Json)].Serialization.JsonConverterFactory>.
  • Zastąp metodę CanConvert, aby zwracał wartość true, gdy typ do przekonwertowania jest taki, który może obsłużyć konwerter.Override the CanConvert method to return true when the type to convert is one that the converter can handle. Na przykład, jeśli konwerter jest przeznaczony dla List<T> może obsłużyć tylko List<int>, List<string>i List<DateTime>.For example, if the converter is for List<T> it might only handle List<int>, List<string>, and List<DateTime>.
  • Zastąp metodę CreateConverter, aby zwracała wystąpienie klasy konwertera, która będzie obsługiwała typ do konwersji, który jest dostarczany w czasie wykonywania.Override the CreateConverter method to return an instance of a converter class that will handle the type-to-convert that is provided at runtime.
  • Utwórz klasę konwertera, którą tworzy instancja metody CreateConverter.Create the converter class that the CreateConverter method instantiates.

Wzorzec fabryki jest wymagany do otwierania typów ogólnych, ponieważ kod do konwersji obiektu na i z ciągu nie jest taki sam dla wszystkich typów.The factory pattern is required for open generics because the code to convert an object to and from a string isn't the same for all types. Konwerter dla otwartego typu ogólnego (na przykładList<T>) musi utworzyć konwerter dla zamkniętego typu ogólnego (na przykładList<DateTime>) w tle.A converter for an open generic type (List<T>, for example) has to create a converter for a closed generic type (List<DateTime>, for example) behind the scenes. Kod musi być zapisany, aby obsługiwał każdy zamknięty typ ogólny, który może obsłużyć konwerter.Code must be written to handle each closed-generic type that the converter can handle.

Typ Enum jest podobny do otwartego typu ogólnego: konwerter dla Enum musi utworzyć konwerter dla konkretnej Enum``WeekdaysEnum(na przykład) w tle.The Enum type is similar to an open generic type: a converter for Enum has to create a converter for a specific Enum (WeekdaysEnum, for example) behind the scenes.

Obsługa błędówError handling

Jeśli musisz zgłosić wyjątek w kodzie obsługi błędu, rozważ przegenerowanie <xref:[!OP.NO-LOC(System.Text.Json)].JsonException> bez komunikatu.If you need to throw an exception in error-handling code, consider throwing a <xref:[!OP.NO-LOC(System.Text.Json)].JsonException> without a message. Ten typ wyjątku powoduje automatyczne utworzenie komunikatu zawierającego ścieżkę do części JSON, która spowodowała błąd.This exception type automatically creates a message that includes the path to the part of the JSON that caused the error. Na przykład instrukcja throw new JsonException(); generuje komunikat o błędzie, jak w poniższym przykładzie:For example, the statement throw new JsonException(); produces an error message like the following example:

Unhandled exception. [!OP.NO-LOC(System.Text.Json)].JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.

Jeśli zostanie wyświetlony komunikat (na przykład throw new JsonException("Error occurred"), wyjątek nadal udostępnia ścieżkę we właściwości <xref:[!OP.NO-LOC(System.Text.Json)].JsonException.Path>.If you do provide a message (for example, throw new JsonException("Error occurred"), the exception still provides the path in the <xref:[!OP.NO-LOC(System.Text.Json)].JsonException.Path> property.

Rejestrowanie niestandardowego konwerteraRegister a custom converter

Zarejestruj konwerter niestandardowy, aby zastosować Serialize i Deserialize metody.Register a custom converter to make the Serialize and Deserialize methods use it. Wybierz jedną z następujących metod:Choose one of the following approaches:

  • Dodaj wystąpienie klasy Converter do kolekcji <xref:[!OP.NO-LOC(System.Text.Json)].JsonSerializerOptions.Converters?displayProperty=nameWithType>.Add an instance of the converter class to the <xref:[!OP.NO-LOC(System.Text.Json)].JsonSerializerOptions.Converters?displayProperty=nameWithType> collection.
  • Zastosuj atrybut [JsonConverter] do właściwości, które wymagają konwertera niestandardowego.Apply the [JsonConverter] attribute to the properties that require the custom converter.
  • Zastosuj atrybut [JsonConverter] do klasy lub struktury, która reprezentuje niestandardowy typ wartości.Apply the [JsonConverter] attribute to a class or a struct that represents a custom value type.

Przykład rejestracji — kolekcja konwerterówRegistration sample - Converters collection

Oto przykład, który sprawia, że DateTimeOffsetConverter domyślny dla właściwości typu DateTimeOffset:Here's an example that makes the DateTimeOffsetConverter the default for properties of type DateTimeOffset:

var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new DateTimeOffsetConverter());
serializeOptions.WriteIndented = true;
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Załóżmy, że Serializacja wystąpienia następującego typu:Suppose you serialize an instance of the following type:

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, które pokazują, że użyto niestandardowego konwertera:Here's an example of JSON output that shows the custom converter was used:

{
  "Date": "08/01/2019",
  "TemperatureCelsius": 25,
  "Summary": "Hot"
}

Poniższy kod używa tego samego podejścia do deserializacji przy użyciu niestandardowego konwertera DateTimeOffset:The following code uses the same approach to deserialize using the custom DateTimeOffset converter:

var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new DateTimeOffsetConverter());
weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions);

Przykład rejestracji — [JsonConverter] na właściwościRegistration sample - [JsonConverter] on a property

Poniższy kod wybiera niestandardowy konwerter właściwości Date:The following code selects a custom converter for the Date property:

public class WeatherForecastWithConverterAttribute
{
    [JsonConverter(typeof(DateTimeOffsetConverter))]
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

Kod do serializacji WeatherForecastWithConverterAttribute nie wymaga użycia JsonSerializeOptions.Converters:The code to serialize WeatherForecastWithConverterAttribute doesn't require the use of JsonSerializeOptions.Converters:

var serializeOptions = new JsonSerializerOptions();
serializeOptions.WriteIndented = true;
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Kod do deserializacji również nie wymaga użycia Converters:The code to deserialize also doesn't require the use of Converters:

weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithConverterAttribute>(jsonString);

Przykład rejestracji — [JsonConverter] w typieRegistration sample - [JsonConverter] on a type

Oto kod, który tworzy strukturę i stosuje do niej atrybut [JsonConverter]:Here's code that creates a struct and applies the [JsonConverter] attribute to it:

using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    [JsonConverter(typeof(TemperatureConverter))]
    public struct Temperature
    {
        public Temperature(int degrees, bool celsius)
        {
            _degrees = degrees;
            _isCelsius = celsius;
        }
        private bool _isCelsius;
        private int _degrees;
        public int Degrees => _degrees;
        public bool IsCelsius => _isCelsius; 
        public bool IsFahrenheit => !_isCelsius;
        public override string ToString() =>
            $"{_degrees.ToString()}{(_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 niestandardowy konwerter dla poprzedniej struktury:Here's the custom converter for the preceding struct:

using System;
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 [JsonConvert] w strukturze rejestruje niestandardowy konwerter jako domyślny dla właściwości typu Temperature.The [JsonConvert] attribute on the struct registers the custom converter as the default for properties of type Temperature. Konwerter jest automatycznie używany we właściwości TemperatureCelsius następującego typu podczas serializacji lub deserializacji:The converter is automatically used on the TemperatureCelsius property of the following type when you serialize or deserialize it:

public class WeatherForecastWithTemperatureStruct
{
    public DateTimeOffset Date { get; set; }
    public Temperature TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

Pierwszeństwo rejestracji konwerteraConverter registration precedence

Podczas serializacji lub deserializacji konwerter jest wybierany dla każdego elementu JSON w następującej kolejności, na podstawie najwyższego priorytetu:During serialization or deserialization, a converter is chosen for each JSON element in the following order, listed from highest priority to lowest:

  • [JsonConverter] zastosowana do właściwości.[JsonConverter] applied to a property.
  • Konwerter dodany do kolekcji Converters.A converter added to the Converters collection.
  • [JsonConverter] zastosowana do niestandardowego typu wartości lub POCO.[JsonConverter] applied to a custom value type or POCO.

Jeśli wiele konwerterów niestandardowych dla typu są zarejestrowane w kolekcji Converters, zostanie użyty pierwszy konwerter zwracający wartość true dla CanConvert.If multiple custom converters for a type are registered in the Converters collection, the first converter that returns true for CanConvert is used.

Wbudowany konwerter jest wybierany tylko wtedy, gdy nie zarejestrowano żadnego odpowiedniego konwertera niestandardowego.A built-in converter is chosen only if no applicable custom converter is registered.

Przykłady konwerterów dla typowych scenariuszyConverter samples for common scenarios

Poniższe sekcje zawierają przykłady konwerterów, które dotyczą niektórych typowych scenariuszy, które nie obsługują funkcji wbudowanych.The following sections provide converter samples that address some common scenarios that built-in functionality doesn't handle.

Deserializacja wywnioskowanych typów do właściwości obiektuDeserialize inferred types to object properties

Podczas deserializacji do właściwości typu objectzostanie utworzony obiekt JsonElement.When deserializing to a property of type object, a JsonElement object is created. Przyczyną jest to, że Deserializator nie wie, jakie typy CLR należy utworzyć, i nie próbuje go odgadnąć.The reason is that the deserializer doesn't know what CLR type to create, and it doesn't try to guess. Na przykład, jeśli właściwość JSON ma wartość "true", Deserializator nie uzna, że wartość jest Boolean, a jeśli element ma "01/01/2019", Deserializator nie uzna, że jest to DateTime.For example, if a JSON property has "true", the deserializer doesn't infer that the value is a Boolean, and if an element has "01/01/2019", the deserializer doesn't infer that it's a DateTime.

Wnioskowanie o typie może być niedokładne.Type inference can be inaccurate. Jeśli Deserializator analizuje numer JSON, który nie ma punktu dziesiętnego jako long, co może spowodować problemy poza zakresem, jeśli wartość została pierwotnie zserializowana jako ulong lub BigInteger.If the deserializer parses a JSON number that has no decimal point as a long, that might result in out-of-range issues if the value was originally serialized as a ulong or BigInteger. Analizowanie liczby, która ma przecinek dziesiętny jako double może stracić dokładnooć, jeśli liczba została pierwotnie zserializowana jako decimal.Parsing a number that has a decimal point as a double might lose precision if the number was originally serialized as a decimal.

W przypadku scenariuszy, które wymagają wnioskowania o typie, poniższy kod przedstawia niestandardowy konwerter dla object właściwości.For scenarios that require type inference, the following code shows a custom converter for object properties. Kod konwertuje:The code converts:

  • true i false do Booleantrue and false to Boolean
  • Liczby bez wartości dziesiętnej do longNumbers without a decimal to long
  • Liczby z liczbą dziesiętną do doubleNumbers with a decimal to double
  • Daty do DateTimeDates to DateTime
  • Ciągi do stringStrings to string
  • Wszystkie inne elementy do JsonElementEverything else to JsonElement
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class ObjectToInferredTypesConverter
        : JsonConverter<object>
    {
        public override object Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.True)
            {
                return true;
            }

            if (reader.TokenType == JsonTokenType.False)
            {
                return false;
            }

            if (reader.TokenType == JsonTokenType.Number)
            {
                if (reader.TryGetInt64(out long l))
                {
                    return l;
                }

                return reader.GetDouble();
            }

            if (reader.TokenType == JsonTokenType.String)
            {
                if (reader.TryGetDateTime(out DateTime datetime))
                {
                    return datetime;
                }

                return reader.GetString();
            }

            // Use JsonElement as fallback.
            // Newtonsoft uses JArray or JObject.
            using JsonDocument document = JsonDocument.ParseValue(ref reader);
            return document.RootElement.Clone();
        }

        public override void Write(
            Utf8JsonWriter writer,
            object objectToWrite,
            JsonSerializerOptions options) =>
                throw new InvalidOperationException("Should not get here.");
    }
}

Następujący kod rejestruje konwerter:The following code registers the converter:

var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new ObjectToInferredTypesConverter());

Oto przykładowy typ z właściwościami object:Here's an example type with object properties:

public class WeatherForecastWithObjectProperties
{
    public object Date { get; set; }
    public object TemperatureCelsius { get; set; }
    public object Summary { get; set; }
}

Poniższy przykład JSON do deserializacji zawiera wartości, które zostaną rozszeregowane jako DateTime, longi string:The following example of JSON to deserialize contains values that will be deserialized as DateTime, long, and string:

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
}

Bez niestandardowego konwertera deserializacja umieszcza JsonElement w każdej właściwości.Without the custom converter, deserialization puts a JsonElement in each property.

Folder testów jednostkowych w przestrzeni nazw System.Text.Json.Serialization zawiera więcej przykładów niestandardowych konwerterów, które obsługują deserializacji do właściwości object.The unit tests folder in the System.Text.Json.Serialization namespace has more examples of custom converters that handle deserialization to object properties.

Obsługa słownika z kluczem niebędącym ciągiemSupport Dictionary with non-string key

Wbudowana obsługa kolekcji słowników ma na celu Dictionary<string, TValue>.The built-in support for dictionary collections is for Dictionary<string, TValue>. Oznacza to, że klucz musi być ciągiem.That is, the key must be a string. Aby zapewnić obsługę słownika z liczbą całkowitą lub innym typem jako klucz, wymagany jest konwerter niestandardowy.To support a dictionary with an integer or some other type as the key, a custom converter is required.

Poniższy kod przedstawia niestandardowy konwerter, który działa z Dictionary<Enum,TValue>:The following code shows a custom converter that works with Dictionary<Enum,TValue>:

using System;
using System.Collections.Generic;
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 keyType = type.GetGenericArguments()[0];
            Type valueType = type.GetGenericArguments()[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    new Type[] { keyType, valueType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: new object[] { options },
                culture: null);

            return converter;
        }

        private class DictionaryEnumConverterInner<TKey, TValue> : 
            JsonConverter<Dictionary<TKey, TValue>> where TKey : struct, Enum
        {
            private readonly JsonConverter<TValue> _valueConverter;
            private Type _keyType;
            private Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter if available.
                _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();
                }

                Dictionary<TKey, TValue> 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.
                    TValue v;
                    if (_valueConverter != null)
                    {
                        reader.Read();
                        v = _valueConverter.Read(ref reader, _valueType, options);
                    }
                    else
                    {
                        v = JsonSerializer.Deserialize<TValue>(ref reader, options);
                    }

                    // Add to dictionary.
                    dictionary.Add(key, v);
                }

                throw new JsonException();
            }

            public override void Write(
                Utf8JsonWriter writer, 
                Dictionary<TKey, TValue> dictionary, 
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();

                foreach (KeyValuePair<TKey, TValue> kvp in dictionary)
                {
                    writer.WritePropertyName(kvp.Key.ToString());

                    if (_valueConverter != null)
                    {
                        _valueConverter.Write(writer, kvp.Value, options);
                    }
                    else
                    {
                        JsonSerializer.Serialize(writer, kvp.Value, options);
                    }
                }

                writer.WriteEndObject();
            }
        }
    }
}

Następujący kod rejestruje konwerter:The following code registers the converter:

var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new DictionaryTKeyEnumTValueConverter());

Konwerter może serializować i deserializować Właściwość TemperatureRanges poniższej klasy, która używa następujących Enum:The converter can serialize and deserialize the TemperatureRanges property of the following class that uses the following Enum:

public class WeatherForecastWithEnumDictionary
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
    public Dictionary<SummaryWordsEnum, int> TemperatureRanges { get; set; }
}

public enum SummaryWordsEnum
{
    Cold, Hot
}

Dane wyjściowe JSON z serializacji wyglądają podobnie jak w poniższym przykładzie:The JSON output from serialization looks like the following example:

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
  "TemperatureRanges": {
    "Cold": 20,
    "Hot": 40
  }
}

Folder testy jednostkowe w przestrzeni nazw System.Text.Json.Serialization zawiera więcej przykładów niestandardowych konwerterów, które obsługują słowniki niebędące ciągami.The unit tests folder in the System.Text.Json.Serialization namespace has more examples of custom converters that handle non-string-key dictionaries.

Obsługa deserializacji polimorficznaSupport polymorphic deserialization

Wbudowane funkcje zapewniają ograniczony zakres serializacji polimorficznej , ale nie obsługują deserializacji.Built-in features provide a limited range of polymorphic serialization but no support for deserialization at all. Deserializacja wymaga konwertera niestandardowego.Deserialization requires a custom converter.

Załóżmy na przykład, że masz Person abstrakcyjną klasę bazową z Employee i Customer klasami pochodnymi.Suppose, for example, you have a Person abstract base class, with Employee and Customer derived classes. Deserializacja polimorficzna oznacza, że w czasie projektowania można określić Person jako element docelowy deserializacji, a Customer i Employee obiektów w formacie JSON są prawidłowo deserializowane w czasie wykonywania.Polymorphic deserialization means that at design time you can specify Person as the deserialization target, and Customer and Employee objects in the JSON are correctly deserialized at runtime. Podczas deserializacji należy znaleźć wskazówki, które identyfikują wymagany typ w kodzie JSON.During deserialization, you have to find clues that identify the required type in the JSON. Rodzaje dostępnych wskazówek różnią się w zależności od scenariusza.The kinds of clues available vary with each scenario. Na przykład może być dostępna właściwość rozróżniacza lub może zależeć od obecności lub braku określonej właściwości.For example, a discriminator property might be available or you might have to rely on the presence or absence of a particular property. Bieżąca wersja System.Text.Json nie udostępnia atrybutów, aby określić sposób obsługi scenariuszy deserializacji polimorficznych, dlatego wymagane są niestandardowe konwertery.The current release of System.Text.Json doesn't provide attributes to specify how to handle polymorphic deserialization scenarios, so custom converters are required.

Poniższy kod przedstawia klasę bazową, dwie klasy pochodne i niestandardowy konwerter dla nich.The following code shows a base class, two derived classes, and a custom converter for them. Konwerter używa właściwości rozróżniacza do wykonywania deserializacji polimorficznej.The converter uses a discriminator property to do polymorphic deserialization. Rozróżniacz typu nie znajduje się w definicjach klas, ale jest tworzony podczas serializacji i jest odczytywany podczas deserializacji.The type discriminator isn't in the class definitions but is created during serialization and is read during deserialization.

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;
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();
        }
    }
}

Następujący kod rejestruje konwerter:The following code registers the converter:

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:The converter can deserialize JSON that was created by using the same converter to serialize, for example:

[
  {
    "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.The converter code in the preceding example reads and writes each property manually. Alternatywą jest wywołanie Deserialize lub Serialize do wykonania niektórych zadań.An alternative is to call Deserialize or Serialize to do some of the work. Aby zapoznać się z przykładem, zobacz ten StackOverflow post.For an example, see this StackOverflow post.

Inne przykłady konwerterów niestandardowychOther custom converter samples

Migracja z Newtonsoft.Json do System.Text.Json artykułu zawiera dodatkowe przykłady konwerterów niestandardowych.The Migrate from Newtonsoft.Json to System.Text.Json article contains additional samples of custom converters.

Folder testy jednostkowe w System.Text.Json.Serialization kodzie źródłowym zawiera inne niestandardowe przykłady konwerterów, takie jak:The unit tests folder in the System.Text.Json.Serialization source code includes other custom converter samples, such as:

Jeśli musisz utworzyć konwerter, który modyfikuje zachowanie istniejącego wbudowanego konwertera, możesz uzyskać kod źródłowy istniejącego konwertera , który będzie używany jako punkt wyjścia do dostosowania.If you need to make a converter that modifies the behavior of an existing built-in converter, you can get the source code of the existing converter to serve as a starting point for customization.

Dodatkowe zasobyAdditional resources