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.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. Na przykład możesz chcieć DateTime , aby wartości były 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.
  • Aby obsługiwać typ wartości niestandardowej. Na przykład PhoneNumber struktura.

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:

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:

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

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 Dictionary elementu z dwoma parametrami ogólnymi, z których pierwszy jest typem Enumtrue. Konwerter wewnętrzny pobiera istniejący konwerter do obsługi niezależnie od typu udostępnianego w czasie wykonywania dla TValueprogramu .

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 typ T. 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 typu T. Użyj metody przekazanej Utf8JsonWriter do metody , aby zapisać kod JSON.
  • Zastąpij metodę CanConvert tylko w razie potrzeby. Domyślna implementacja zwraca true wartość, gdy typ do konwersji ma typ T. W związku z tym konwertery obsługujące tylko typ T 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ą CanConverttrue , gdy typ do konwersji jest taki, który konwerter może obsłużyć. Jeśli na przykład konwerter jest przeznaczony dla List<T>elementu , może obsługiwać tylko elementy List<int>, List<string>i List<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 (WeekdaysEnumna 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:

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,
    Converters =
    {
        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ą.

Aby zapoznać się z przykładowym DataTable konwerterem, zobacz Obsługiwane typy kolekcji.

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:

  • true i false do Boolean
  • 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 CustomerEmployee 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 oryginalnego Reader wystąpienia, gdy znasz potrzebny typ. Można wywołać metodę Deserialize , ponieważ oryginalne Reader 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
{
    Converters = { new JsonConverterFactoryForStackOfT() },
};

Zasady nazewnictwa na potrzeby deserializacji ciągów wyliczenia

Domyślnie wbudowana JsonStringEnumConverter funkcja może serializować i deserializować wartości ciągów dla wyliczenia. Działa bez określonych zasad nazewnictwa lub zasad CamelCase nazewnictwa. Nie obsługuje innych zasad nazewnictwa, takich jak przypadek węża. Aby uzyskać informacje o niestandardowym kodzie konwertera, który może obsługiwać zaokrąglanie do i z wartości ciągów wyliczenia podczas korzystania z zasad nazewnictwa przypadków węża, zobacz Problem z usługą GitHub dotnet/runtime #31619. Alternatywnie uaktualnij program do wersji .NET 7 lub nowszej, która zapewnia wbudowaną obsługę stosowania zasad nazewnictwa podczas zaokrąglania do i z wartości ciągów wyliczeniowych.

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.
  • 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ć 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 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/Deserializewywoł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 , czyli Employee. 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.

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:

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.

Dodatkowe zasoby