Jak psát vlastní převaděče pro serializaci JSON (zařazování) v .NET

Tento článek ukazuje, jak vytvořit vlastní převaděče pro třídy serializace JSON, které jsou poskytovány System.Text.Json v oboru názvů. Úvod do System.Text.Jsontématu Jak serializovat a deserializovat JSON v .NET.

Převaděč je třída, která převádí objekt nebo hodnotu do a z JSON. Obor System.Text.Json názvů obsahuje integrované převaděče pro většinu primitivních typů, které se mapují na primitiva JavaScriptu. Můžete napsat vlastní převaděče, které přepíší výchozí chování integrovaného převaděče. Příklad:

  • Můžete chtít DateTime , aby hodnoty byly reprezentovány formátem mm/dd/rrrr. Ve výchozím nastavení se podporuje ISO 8601-1:2019, včetně profilu RFC 3339. Další informace naleznete v tématu DateTime a DateTimeOffset podpora v System.Text.Json.
  • Můžete chtít serializovat POCO jako řetězec JSON, například s typem PhoneNumber .

Můžete také psát vlastní převaděče pro přizpůsobení nebo rozšíření System.Text.Json o nové funkce. Následující scénáře jsou popsané dále v tomto článku:

Jazyk Visual Basic se nedá použít k psaní vlastních převaděčů, ale může volat převaděče implementované v knihovnách jazyka C#. Další informace naleznete v tématu Podpora jazyka Visual Basic.

Vzory vlastních převaděčů

Existují dva vzory pro vytvoření vlastního převaděče: základní vzor a vzor továrny. Vzor továrny je určen pro převaděče, které zpracovávají typ Enum nebo otevírají obecné typy. Základní vzor je určený pro obecné a uzavřené obecné typy. Například převaděče pro následující typy vyžadují vzor továrny:

Mezi příklady typů, které je možné zpracovat základním vzorem, patří:

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

Základní vzor vytvoří třídu, která dokáže zpracovat jeden typ. Vzor továrny vytvoří třídu, která určuje, v době běhu, který konkrétní typ je vyžadován a dynamicky vytvoří příslušný převaděč.

Ukázkový základní převaděč

Následující ukázka je převaděč, který přepíše výchozí serializaci pro existující datový typ. Převaděč používá pro DateTimeOffset vlastnosti formát mm/dd/rrrr.

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

Převaděč vzorů ukázkové továrny

Následující kód ukazuje vlastní převaděč, který funguje s Dictionary<Enum,TValue>. Kód se řídí vzorem továrny, protože první parametr obecného typu je Enum a druhý je otevřený. Metoda CanConvert vrátí true pouze pro Dictionary dva obecné parametry, z nichž první je Enum typ. Vnitřní převaděč získá existující převaděč pro zpracování toho, který typ je poskytován za běhu pro 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();
            }
        }
    }
}

Postup základního vzoru

Následující kroky vysvětlují, jak vytvořit převaděč pomocí základního vzoru:

  • Vytvořte třídu, která je odvozena od JsonConverter<T> where T je typ, který má být serializován a deserializován.
  • Přepsat metodu Read deserializovat příchozí JSON a převést ji na typ T. Utf8JsonReader Použijte metodu předanou ke čtení JSON. Nemusíte se starat o zpracování částečných dat, protože serializátor předává všechna data pro aktuální obor JSON. Proto není nutné volat Skip nebo TrySkip ověřit, že Read se vrátí true.
  • Přepsat metodu Write serializovat příchozí objekt typu T. Utf8JsonWriter Použijte metodu předanou k zápisu JSON.
  • Přepsat metodu CanConvert pouze v případě potřeby. Výchozí implementace vrátí true , když typ převodu je typu T. Převaděče, které podporují pouze typ T , proto tuto metodu nemusí přepsat. Příklad převaděče, který potřebuje tuto metodu přepsat, naleznete v části polymorfní deserializace dále v tomto článku.

Můžete odkazovat na předdefinované převaděče zdrojový kód jako referenční implementace pro psaní vlastních převaděčů.

Postup použití vzoru továrny

Následující kroky vysvětlují, jak vytvořit převaděč podle vzoru továrny:

  • Vytvoření třídy, která je odvozena od JsonConverterFactory.
  • Přepište metodu CanConvert , která se má vrátit true , když typ převodu je ten, který převaděč dokáže zpracovat. Například pokud je převaděč pro List<T>, může zpracovat List<int>pouze , List<string>a List<DateTime>.
  • Přepište metodu CreateConverter pro vrácení instance třídy převaděče, která bude zpracovávat typ-to-convert, který je k dispozici za běhu.
  • Vytvořte třídu převaděče, kterou CreateConverter metoda vytvoří instanci.

Vzor továrny se vyžaduje pro otevřené obecné typy, protože kód pro převod objektu na řetězec a z řetězce není stejný pro všechny typy. Převaděč pro otevřený obecný typ (List<T>například) musí vytvořit převaděč pro uzavřený obecný typ (List<DateTime>například) na pozadí. Kód musí být zapsán pro zpracování každého uzavřeného obecného typu, který převaděč dokáže zpracovat.

Typ Enum je podobný otevřenému obecnému typu: převaděč pro Enum vytvoření převaděče pro konkrétní Enum (WeekdaysEnumnapříklad) na pozadí.

Použití Utf8JsonReader v Read metodě

Pokud převaděč převádí objekt JSON, umístí se Utf8JsonReader na počáteční token objektu při Read zahájení metody. Pak musíte přečíst všechny tokeny v daném objektu a ukončit metodu se čtečkou umístěnou na odpovídajícím koncovém tokenu objektu. Pokud před dosažením odpovídajícího koncového tokenu přečtete za konec objektu nebo pokud se zastavíte, zobrazí se JsonException výjimka, která značí, že:

Převaděč Converter 'ConverterName' číst příliš mnoho nebo nestačí.

Příklad najdete v ukázkovém převaděči vzorů předchozí továrny. Metoda Read začíná ověřením, že je čtenář umístěn na počáteční token objektu. Přečte, dokud nenajde, že je umístěn na dalším koncovém tokenu objektu. Zastaví se na dalším koncovém tokenu objektu, protože neexistují žádné počáteční tokeny objektu, které by označovaly objekt uvnitř objektu. Stejné pravidlo o počátečním a koncovém tokenu platí, pokud převádíte pole. Příklad najdete v ukázkovém Stack<T> převaděči dále v tomto článku.

Zpracování chyb

Serializátor poskytuje speciální zpracování pro typy JsonException výjimek a NotSupportedException.

JsonException

Pokud vyvoláte JsonException zprávu bez zprávy, serializátor vytvoří zprávu, která obsahuje cestu k části JSON, která způsobila chybu. Příkaz throw new JsonException() například vytvoří chybovou zprávu jako v následujícím příkladu:

Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.

Pokud zadáte zprávu (například throw new JsonException("Error occurred")), serializátor stále nastaví Path, LineNumbera BytePositionInLine vlastnosti.

Notsupportedexception

Pokud vyvoláte zprávu NotSupportedException, vždy získáte informace o cestě ve zprávě. Pokud zadáte zprávu, připojí se k ní informace o cestě. Příkaz throw new NotSupportedException("Error occurred.") například vytvoří chybovou zprávu jako v následujícím příkladu:

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

Kdy vyvolat typ výjimky

Pokud datová část JSON obsahuje tokeny, které nejsou platné pro deserializovaný typ, vyvolte výjimku JsonException.

Pokud chcete zakázat určité typy, vyhoďte .NotSupportedException Tato výjimka je to, co serializátor automaticky vyvolá pro typy, které nejsou podporovány. Například System.Type není podporován z bezpečnostních důvodů, takže pokus o deserializaci má za NotSupportedExceptionnásledek .

Podle potřeby můžete vyvolat další výjimky, ale neobsahují informace o cestě JSON automaticky.

Registrace vlastního převaděče

Zaregistrujte vlastní převaděč, aby ho Serialize používaly a Deserialize používaly. Zvolte jeden z následujících přístupů:

Ukázka registrace – Kolekce převaděčů

Tady je příklad, který nastaví DateTimeOffsetJsonConverter jako výchozí pro vlastnosti typu DateTimeOffset:

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true,
    Converters =
    {
        new DateTimeOffsetJsonConverter()
    }
};

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Předpokládejme, že serializujete instanci následujícího typu:

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

Tady je příklad výstupu JSON, který ukazuje použití vlastního převaděče:

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

Následující kód používá stejný přístup k deserializaci pomocí vlastního DateTimeOffset převaděče:

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

Ukázka registrace – [JsonConverter] u vlastnosti

Následující kód vybere vlastní převaděč vlastnosti Date :

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

Kód k serializaci WeatherForecastWithConverterAttribute nevyžaduje použití JsonSerializeOptions.Converters:

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

Kód pro deserializaci také nevyžaduje použití Converters:

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

Ukázka registrace – [JsonConverter] u typu

Tady je kód, který vytvoří strukturu a použije [JsonConverter] na ni atribut:

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

Tady je vlastní převaděč pro předchozí strukturu:

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

Atribut [JsonConverter] ve struktuře registruje vlastní převaděč jako výchozí pro vlastnosti typu Temperature. Převaděč se automaticky používá u TemperatureCelsius vlastnosti následujícího typu při serializaci nebo deserializaci:

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

Priorita registrace převaděče

Během serializace nebo deserializace je převaděč vybrán pro každý prvek JSON v následujícím pořadí, uvedený od nejvyšší priority po nejnižší:

  • [JsonConverter] použitý u vlastnosti.
  • Převaděč přidaný do Converters kolekce.
  • [JsonConverter] použitý pro vlastní typ hodnoty nebo POCO.

Pokud je v Converters kolekci registrováno více vlastních převaděčů pro určitý typ, použije se první převaděč, který se vrátí true pro CanConvert daný typ.

Integrovaný převaděč je zvolen pouze v případě, že není registrován žádný použitelný vlastní převaděč.

Ukázky převaděčů pro běžné scénáře

Následující části obsahují ukázky převaděčů, které řeší některé běžné scénáře, které integrované funkce nezpracují.

Ukázkový DataTable převaděč naleznete v tématu Podporované typy kolekcí.

Deserializace odvozených typů na vlastnosti objektu

Při deserializaci na vlastnost typu object, JsonElement objekt je vytvořen. Důvodem je, že deserializátor neví, jaký typ CLR se má vytvořit, a nepokouší se odhadnout. Pokud má například vlastnost JSON hodnotu true, deserializer neodpočítá, že hodnota je a Booleanpokud prvek má hodnotu 01/01/2019, deserializátor neodpočítá, že se jedná o DateTime.

Odvození typu může být nepřesné. Pokud deserializátor parsuje číslo JSON, které nemá jako desetinnou čárku long, může vést k problémům mimo rozsah, pokud byla hodnota původně serializována jako ulong nebo BigInteger. Analýza čísla, které má desetinnou čárku jako desetinnou čárku double , může přijít o přesnost, pokud bylo číslo původně serializováno jako decimal.

V případě scénářů, které vyžadují odvození typu, následující kód zobrazí vlastní převaděč vlastností object . Kód převede:

  • true a false do Boolean
  • Čísla bez desetinné čárky long
  • Čísla s desetinným číslem na double
  • Data do DateTime
  • Řetězce pro string
  • Všechno ostatní na 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"
//}

Příklad ukazuje kód převaděče a WeatherForecast třídu s vlastnostmi object . Metoda Main deserializuje řetězec JSON do WeatherForecast instance, nejprve bez použití převaděče a pak pomocí převaděče. Výstup konzoly ukazuje, že bez převaděče je typ běhu vlastnosti DateJsonElement; s převaděčem, typ běhu je DateTime.

Složka testů jednotek v System.Text.Json.Serialization oboru názvů obsahuje více příkladů vlastních převaděčů, které zpracovávají deserializaci na object vlastnosti.

Podpora polymorfní deserializace

.NET 7 poskytuje podporu polymorfní serializace i deserializace. V předchozích verzích .NET však byla omezena podpora polymorfní serializace a nebyla podporována deserializace. Pokud používáte .NET 6 nebo starší verzi, deserializace vyžaduje vlastní převaděč.

Předpokládejme například, že máte Person abstraktní základní třídu s odvozenými třídami a Customer odvozenými třídamiEmployee. Polymorfní deserializace znamená, že v době návrhu můžete zadat Person jako cíl deserializace a CustomerEmployee objekty ve formátu JSON jsou správně deserializovány za běhu. Během deserializace musíte najít vodítka, která identifikují požadovaný typ ve formátu JSON. Různé druhy nápovědy, které jsou k dispozici, se liší v každém scénáři. Například může být k dispozici diskriminující vlastnost nebo se budete muset spolehnout na přítomnost nebo nepřítomnost konkrétní vlastnosti. Aktuální verze System.Text.Json neposkytuje atributy, které určují, jak zpracovávat polymorfní deserializační scénáře, takže jsou vyžadovány vlastní převaděče.

Následující kód ukazuje základní třídu, dvě odvozené třídy a vlastní převaděč pro ně. Převaděč používá diskriminující vlastnost k polymorfní deserializaci. Typ diskriminátor není v definicích třídy, ale je vytvořen během serializace a je čtena během deserializace.

Důležité

Ukázkový kód vyžaduje, aby páry názvu a hodnoty objektu JSON zůstaly v pořádku, což není standardní požadavek 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();
        }
    }
}

Následující kód zaregistruje převaděč:

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

Převaděč může deserializovat JSON, který byl vytvořen pomocí stejného převaděče pro serializaci, například:

[
  {
    "TypeDiscriminator": 1,
    "CreditLimit": 10000,
    "Name": "John"
  },
  {
    "TypeDiscriminator": 2,
    "OfficeNumber": "555-1234",
    "Name": "Nancy"
  }
]

Kód převaděče v předchozím příkladu čte a zapisuje každou vlastnost ručně. Alternativou je volání Deserialize nebo Serialize provedení některé práce. Příklad najdete v tomto příspěvku StackOverflow.

Alternativní způsob polymorfní deserializace

Metodu Read můžete volatDeserialize:

  • Vytvořte klon Utf8JsonReader instance. Vzhledem k tomu Utf8JsonReader , že se jedná o strukturu, stačí příkaz přiřazení.
  • Pomocí klonu můžete číst diskriminující tokeny.
  • Jakmile znáte typ, který potřebujete, zavolejte Deserialize pomocí původní Reader instance. Můžete volat Deserialize , protože původní Reader instance je stále umístěna ke čtení počátečního tokenu objektu.

Nevýhodou této metody je, že nemůžete předat původní instanci možností, která registruje převaděč do Deserialize. To by způsobilo přetečení zásobníku, jak je vysvětleno v povinných vlastnostech. Následující příklad ukazuje metodu Read , která používá tuto alternativu:

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

Podpora odezvy pro Stack typy

Pokud deserializujete řetězec JSON do objektu Stack a potom serializujete tento objekt, obsah zásobníku je v obráceném pořadí. Toto chování platí pro následující typy a rozhraní a uživatelem definované typy, které jsou z nich odvozeny:

K podpoře serializace a deserializace, která zachovává původní pořadí v zásobníku, je vyžadován vlastní převaděč.

Následující kód ukazuje vlastní převaděč, který umožňuje přecházení do a z Stack<T> objektů:

using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class JsonConverterFactoryForStackOfT : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
            => typeToConvert.IsGenericType
            && typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>);

        public override JsonConverter CreateConverter(
            Type typeToConvert, JsonSerializerOptions options)
        {
            Debug.Assert(typeToConvert.IsGenericType &&
                typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>));

            Type elementType = typeToConvert.GetGenericArguments()[0];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(JsonConverterForStackOfT<>)
                    .MakeGenericType(new Type[] { elementType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: null,
                culture: null)!;

            return converter;
        }
    }

    public class JsonConverterForStackOfT<T> : JsonConverter<Stack<T>>
    {
        public override Stack<T> Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartArray)
            {
                throw new JsonException();
            }
            reader.Read();

            var elements = new Stack<T>();

            while (reader.TokenType != JsonTokenType.EndArray)
            {
                elements.Push(JsonSerializer.Deserialize<T>(ref reader, options)!);

                reader.Read();
            }

            return elements;
        }

        public override void Write(
            Utf8JsonWriter writer, Stack<T> value, JsonSerializerOptions options)
        {
            writer.WriteStartArray();

            var reversed = new Stack<T>(value);

            foreach (T item in reversed)
            {
                JsonSerializer.Serialize(writer, item, options);
            }

            writer.WriteEndArray();
        }
    }
}

Následující kód zaregistruje převaděč:

var options = new JsonSerializerOptions
{
    Converters = { new JsonConverterFactoryForStackOfT() },
};

Zásady pojmenování pro deserializaci řetězců výčtu

Ve výchozím nastavení může předdefinovaný JsonStringEnumConverter serializovat a deserializovat řetězcové hodnoty pro výčty. Funguje bez zadané zásady pojmenování nebo se CamelCase zásadami pojmenování. Nepodporuje jiné zásady pojmenování, jako je případ hada. Informace o kódu vlastního převaděče, který může podporovat zaokrouhlování na řetězcové hodnoty výčtu a hodnoty z nich při používání zásad pojmenování hadů, najdete v tématu problém GitHubu dotnet/runtime #31619. Alternativně upgradujte na .NET 7 nebo novější verze, které poskytují integrovanou podporu pro použití zásad pojmenování při zaokrouhlování na řetězcové hodnoty výčtu a z nich.

Použití výchozího převaděče systému

V některých scénářích můžete chtít použít výchozí převaděč systému ve vlastním převaděči. Chcete-li to provést, získejte systémový převaděč z JsonSerializerOptions.Default vlastnosti, jak je znázorněno v následujícím příkladu:

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

Zpracování hodnot null

Serializátor ve výchozím nastavení zpracovává hodnoty null následujícím způsobem:

  • Pro referenční typy a Nullable<T> typy:

    • Při serializaci nepředává null vlastní převaděče.
    • Při deserializaci se nepředává JsonTokenType.Null do vlastních převaděčů.
    • Vrátí null instanci při deserializaci.
    • Zapisuje null přímo se zapisovačem při serializaci.
  • Pro typy hodnot bez hodnoty null:

    • JsonTokenType.Null Předává se vlastním převaděčům při deserializaci. (Pokud není k dispozici žádný vlastní převaděč, JsonException vyvolá se výjimka interním převaděčem pro typ.)

Toto chování zpracování null je primárně pro optimalizaci výkonu přeskočením nadbytečného volání převaděče. Kromě toho zabraňuje vynucení převaděčů pro typy s možnou hodnotou null kontrolovat null na začátku každého Read přepsání a Write metody.

Chcete-li povolit vlastní převaděč pro zpracování null odkazu nebo typu hodnoty, přepsání JsonConverter<T>.HandleNull vrátit true, jak je znázorněno v následujícím příkladu:

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.

Zachování odkazů

Ve výchozím nastavení se referenční data ukládají pouze do mezipaměti pro každé volání nebo SerializeDeserialize. Chcete-li zachovat odkazy z jednoho Serialize/Deserialize volání do druhého, kořen instance ReferenceResolver v lokalitě Serialize/Deserializevolání . Následující kód ukazuje příklad pro tento scénář:

  • Napíšete vlastní převaděč pro Company typ.
  • Nechcete vlastnost serializovat Supervisor ručně, což je .Employee Chcete tuto možnost delegovat na serializátor a chcete zachovat také odkazy, které jste již uložili.

Tady jsou třídyEmployee:Company

public class Employee
{
    public string? Name { get; set; }
    public Employee? Manager { get; set; }
    public List<Employee>? DirectReports { get; set; }
    public Company? Company { get; set; }
}

public class Company
{
    public string? Name { get; set; }
    public Employee? Supervisor { get; set; }
}

Převaděč vypadá takto:

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

Třída odvozená od ReferenceResolver úložišť odkazů ve slovníku:

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

Třída, která je odvozena z ReferenceHandler blokování instance MyReferenceResolver a vytváří novou instanci pouze v případě potřeby (v metodě pojmenované Reset v tomto příkladu):

class MyReferenceHandler : ReferenceHandler
{
    public MyReferenceHandler() => Reset();

    private ReferenceResolver? _rootedResolver;
    public override ReferenceResolver CreateResolver() => _rootedResolver!;
    public void Reset() => _rootedResolver = new MyReferenceResolver();
}

Když vzorový kód volá serializátor, používá JsonSerializerOptions instanci, ve které ReferenceHandler je vlastnost nastavena na instanci MyReferenceHandler. Když budete postupovat podle tohoto vzoru, nezapomeňte po dokončení serializace obnovit ReferenceResolver slovník, aby se nezvětšil navždy.

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

Předchozí příklad pouze serializace, ale podobný přístup lze přijmout pro deserializaci.

Další ukázky vlastních převaděčů

Migrace z Newtonsoft.JsonSystem.Text.Json článku obsahuje další ukázky vlastních převaděčů.

Složka testů jednotek ve zdrojovém System.Text.Json.Serialization kódu obsahuje další vlastní ukázky převaděčů, například:

Pokud potřebujete vytvořit převaděč, který upravuje chování existujícího integrovaného převaděče, můžete získat zdrojový kód existujícího převaděče , který bude sloužit jako výchozí bod pro přizpůsobení.

Další materiály