Megosztás a következőn keresztül:


Egyéni konverterek írása JSON-szerializáláshoz (rendezéshez) a .NET-ben

Ez a cikk bemutatja, hogyan hozhat létre egyéni konvertereket a névtérben megadott JSON szerializálási System.Text.Json osztályokhoz. A bevezetést a System.Text.JsonJSON szerializálása és deszerializálása a .NET-ben című témakörben talál.

A konverter olyan osztály, amely egy objektumot vagy értéket JSON-ra és JSON-ra konvertál. A System.Text.Json névtér beépített konverterekkel rendelkezik a JavaScript-primitívekhez kapcsolódó legtöbb primitív típushoz. Egyéni konvertereket írhat egy beépített konverter alapértelmezett viselkedésének felülbírálásához. Példa:

  • Előfordulhat, hogy az értékeket mm/dd/yyy formátumban szeretné DateTime ábrázolni. Alapértelmezés szerint az ISO 8601-1:2019 támogatott, beleértve az RFC 3339-profilt is. További információ: DateTime és DateTimeOffset támogatás a System.Text.Json.
  • Előfordulhat, hogy JSON-sztringként szeretne szerializálni egy POCO-t, például egy PhoneNumber típussal.

Egyéni konvertereket is írhat az új funkciók testreszabásához vagy kibővítéséhez System.Text.Json . A cikk későbbi részében a következő forgatókönyveket ismerteti:

A Visual Basic nem használható egyéni konverterek írására, de C#-kódtárakban implementált konvertereket is meghívhat. További információ: Visual Basic-támogatás.

Egyéni konverterminták

Az egyéni konverter létrehozásához két minta létezik: az alapszintű és a gyári minta. A gyári minta olyan konverterekhez készült, amelyek típust Enum vagy nyitott általános generikusokat kezelnek. Az alapminta nem általános és zárt általános típusok esetében használható. A következő típusok konverterei például a gyári mintát igénylik:

Néhány példa az alapszintű mintával kezelhető típusokra:

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

Az alapszintű minta egy olyan osztályt hoz létre, amely képes kezelni egy típust. A gyári minta létrehoz egy osztályt, amely futásidőben meghatározza, hogy melyik típusra van szükség, és dinamikusan létrehozza a megfelelő konvertert.

Minta alapkonverter

Az alábbi minta egy konverter, amely felülbírálja egy meglévő adattípus alapértelmezett szerializálását. A konverter mm/dd/yyy formátumot használ a tulajdonságokhoz DateTimeOffset .

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

Minta-előállító mintakonverter

Az alábbi kód egy egyéni konvertert mutat be, amely a következővel Dictionary<Enum,TValue>működik: A kód a gyári mintát követi, mert az első általános típusparaméter, Enum a második pedig nyitva van. A CanConvert metódus csak két általános paramétert ad trueDictionary vissza, amelyek közül az első típus Enum . A belső konverter egy meglévő konvertert kap, amely a futtatáskor TValuemegadott típust kezeli.

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

Az alapszintű minta követésének lépései

Az alábbi lépések az alapszintű mintát követve ismertetik, hogyan hozhat létre konvertert:

  • Hozzon létre egy osztályt, amely abból a típusból JsonConverter<T>T származik, amelyet szerializálni és deszerializálni kell.
  • Felülbírálja a Read bejövő JSON deszerializálására és típussá Talakítására vonatkozó metódust. A JSON olvasásához használja a Utf8JsonReader metódusnak átadott adatokat. Nem kell aggódnia a részleges adatok kezelése miatt, mivel a szerializáló az aktuális JSON-hatókör összes adatát átadja. Ezért nem szükséges meghívni Skip vagy TrySkip ellenőrizni a Read visszatérést true.
  • Felülbírálja a metódust Write a bejövő típusú objektum szerializálásához T. A JSON írásához használja a Utf8JsonWriter metódusnak átadott parancsot.
  • A metódust CanConvert csak szükség esetén bírálja felül. Az alapértelmezett implementáció akkor tér vissza true , ha az átalakítandó típus típus T. Ezért a csak típust T támogató konvertereknek nem kell felülbírálniuk ezt a módszert. A metódus felülbírálásához szükséges konverterre a cikk későbbi, polimorf deszerializálási szakaszában talál példát.

Az egyéni konverterek írásához referencia-implementációként hivatkozhat a beépített konverterek forráskódjaira .

A gyári minta követésének lépései

Az alábbi lépések bemutatják, hogyan hozhat létre konvertert a gyári minta követésével:

  • Hozzon létre egy osztályt, amely a forrásból JsonConverterFactoryszármazik.
  • Felülbírálja a CanConvert visszatérési true metódust, ha az átalakítandó típus az, amelyet a konverter képes kezelni. Ha például a konverter a következőhöz tartozik, akkor előfordulhat, hogy csak a következőt List<T>kezeliList<int>List<string>List<DateTime>:
  • Felülbírálja a CreateConverter metódust egy konverterosztály egy példányának visszaadásához, amely kezeli a futtatáskor megadott konvertálási típust.
  • Hozza létre a metódus által példányosított konverterosztályt CreateConverter .

A gyári minta a nyitott általánosakhoz szükséges, mert az objektum sztringgé alakítására szolgáló kód nem minden típus esetében ugyanaz. Egy nyitott általános típus konverterének (List<T>például) létre kell hoznia egy konvertert egy zárt általános típushoz (List<DateTime>például) a színfalak mögött. A kódot úgy kell írni, hogy a konverter képes legyen kezelni az összes olyan zárt generikus típust, amelyet a konverter képes kezelni.

A Enum típus hasonló egy nyílt általános típushoz: a konverternek Enum létre kell hoznia egy konvertert egy adott Enum (WeekdaysEnumpéldául) színfalak mögötti konverterhez.

A metódus használata Utf8JsonReaderRead

Ha a konverter JSON-objektumot konvertál, a metódus indításakor Read a Utf8JsonReader rendszer a kezdő objektum jogkivonatán lesz elhelyezve. Ezután át kell olvasnia az objektum összes jogkivonatát, és ki kell lépnie a metódusból, és az olvasót a megfelelő végponti jogkivonaton kell elhelyeznie. Ha az objektum végénél tovább olvas, vagy a megfelelő záró jogkivonat elérése előtt leáll, kivétel jelenik meg JsonException , amely a következőket jelzi:

A "ConverterName" konverter túl sokat olvas vagy nem elég.

Példaként tekintse meg az előző gyári mintakonvertert. A Read metódus első lépéseként ellenőrizze, hogy az olvasó egy kezdőobjektum-jogkivonaton van-e elhelyezve. Addig olvas, amíg meg nem találja, hogy a következő objektumjogkivonaton van elhelyezve. A következő végobjektum-jogkivonat leáll, mert nincsenek olyan beavatkozó kezdőobjektum-jogkivonatok, amelyek egy objektumot jeleznének az objektumon belül. A kezdő jogkivonatra és a záró jogkivonatra ugyanaz a szabály érvényes, ha tömböt konvertál. Példaként tekintse meg a Stack<T> cikk későbbi részében található mintakonvertert.

Hibakezelés

A szerializáló speciális kezelést biztosít a kivételtípusokhoz JsonException és NotSupportedExceptiona .

JsonException

Ha üzenet nélkül küld el egy JsonException üzenetet, a szerializáló létrehoz egy üzenetet, amely tartalmazza a JSON azon részének elérési útját, amely a hibát okozta. Az utasítás throw new JsonException() például a következő példához hasonló hibaüzenetet jelenít meg:

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

Ha mégis megad egy üzenetet (példáulthrow new JsonException("Error occurred")), a szerializáló továbbra is beállítja a , LineNumberés BytePositionInLine a Pathtulajdonságokat.

NotSupportedException

Ha dob egy NotSupportedException, mindig megkapja az elérési út adatait az üzenetben. Ha üzenetet ad meg, az elérési út adatai hozzá lesznek fűzve. Az utasítás throw new NotSupportedException("Error occurred.") például a következő példához hasonló hibaüzenetet jelenít meg:

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

Mikor kell a kivételtípust eldobni?

Ha a JSON hasznos adat olyan jogkivonatokat tartalmaz, amelyek nem érvényesek a deszerializált típusra, dobjon egy JsonException.

Ha le szeretné tiltani bizonyos típusokat, dobjon egy NotSupportedException. Ez a kivétel az, amit a szerializáló automatikusan a nem támogatott típusok esetében dob. Biztonsági okokból például System.Type nem támogatott, ezért a deszerializálási kísérlet egy NotSupportedException.

Szükség szerint más kivételeket is megadhat, de ezek nem tartalmazzák automatikusan a JSON elérési útját.

Egyéni konverter regisztrálása

Regisztráljon egy egyéni konvertert, hogy a metódusok és Deserialize a Serialize metódusok használva legyenek. Válasszon az alábbi módszerek közül:

Regisztrációs minta – Konverterek gyűjteménye

Íme egy példa, amely a DateTimeOffsetJsonConvertert állítja be az alapértelmezett típustulajdonságok DateTimeOffsetközé:

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

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Tegyük fel, hogy a következő típusú példányt szerializálja:

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

Íme egy példa az egyéni konverter használatát szemléltető JSON-kimenetre:

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

Az alábbi kód ugyanezt a módszert használja az egyéni DateTimeOffset konverter használatával történő deszerializáláshoz:

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

Regisztrációs minta – [JsonConverter] tulajdonságon

A következő kód kiválaszt egy egyéni konvertert a Date tulajdonsághoz:

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

A szerializálandó WeatherForecastWithConverterAttribute kód nem igényli a következő használatát JsonSerializeOptions.Converters:

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

A deszerializálni kívánt kódhoz nem szükséges a következő használata Converters:

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

Regisztrációs minta – [JsonConverter] típuson

Az alábbi kód létrehoz egy strukturát, és alkalmazza rá az [JsonConverter] attribútumot:

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

Az előző struktúra egyéni konvertere a következő:

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

A [JsonConverter] szerkezet attribútuma az egyéni konvertert regisztrálja alapértelmezettként a típustulajdonságok Temperatureesetében. A konverter automatikusan az TemperatureCelsius alábbi típusú tulajdonságon lesz használva szerializálásakor vagy deszerializálásakor:

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

Konverterregisztráció elsőbbsége

Szerializálás vagy deszerializálás során minden JSON-elemhez konvertert választunk a következő sorrendben, a legmagasabb prioritástól a legalacsonyabbig:

  • [JsonConverter] tulajdonságra alkalmazva.
  • A gyűjteményhez Converters hozzáadott konverter.
  • [JsonConverter] egyéni értéktípusra vagy POCO-ra alkalmazva.

Ha egy típushoz több egyéni konverter van regisztrálva a Converters gyűjteményben, akkor a rendszer az első konvertert használja, amely visszaadja true azokat CanConvert .

A beépített konvertereket csak akkor választja ki a rendszer, ha nincs regisztrálva a megfelelő egyéni konverter.

Konverterminták gyakori forgatókönyvekhez

A következő szakaszok olyan konvertermintákat nyújtanak, amelyek olyan gyakori forgatókönyveket kezelnek, amelyeket a beépített funkciók nem kezelnek.

Mintakonverter DataTable esetén lásd a támogatott gyűjteménytípusokat.

A kikövetkeztetett típusok deszerializálása objektumtulajdonságokra

Ha egy típustulajdonságra objectdeszerializál, létrejön egy JsonElement objektum. Ennek az az oka, hogy a deszerializáló nem tudja, milyen CLR-típust szeretne létrehozni, és nem próbálja kitalálni. Ha például egy JSON-tulajdonság értéke "true", akkor a deszerializáló nem arra következtet, hogy az érték egy Boolean, és ha egy elem "2019. 01. 01." értékkel rendelkezik, akkor a deszerializáló nem arra következtet, hogy az egy DateTime.

A típus következtetése pontatlan lehet. Ha a deszerializáló olyan JSON-számot elemez, amelynek nincs tizedesvesszője long, az tartományon kívüli problémákat okozhat, ha az érték eredetileg szerializálva ulongBigIntegerlett. Ha a szám eredetileg szerializálva decimallett, akkor a tizedesvesszővel double rendelkező szám elemzése elveszítheti a pontosságot.

Típuskövetkeztetést igénylő forgatókönyvek esetén az alábbi kód egy egyéni konvertert jelenít meg a tulajdonságokhoz object . A kód átalakítja a következőt:

  • true és false a Boolean
  • Tizedesjegy nélküli számok long
  • Számok tizedesjellel double
  • Dátumok DateTime
  • Sztringek a string
  • Minden máshoz 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"
//}

A példa a konverterkódot és a tulajdonságokat tartalmazó osztályt WeatherForecastobject mutatja be. A Main metódus először a konverter használata nélkül, majd a konverter használatával deszerializál egy JSON-sztringet egy WeatherForecast példányba. A konzol kimenete azt mutatja, hogy a konverter nélkül a tulajdonság futásidejének típusa Date ; a konverter esetében a futási idő típusa DateTime.JsonElement

A névtér egységtesztek mappájában további példák találhatók a System.Text.Json.Serialization tulajdonságok deszerializálását object kezelő egyéni konverterekre.

Polimorf deszerializálás támogatása

A .NET 7 támogatja a polimorf szerializálást és a deszerializálást is. A korábbi .NET-verziókban azonban korlátozott volt a polimorf szerializálás támogatása, és nem támogatott a deszerializálás. Ha .NET 6-ot vagy korábbi verziót használ, a deszerializáláshoz egyéni konverterre van szükség.

Tegyük fel például, hogy van egy Person absztrakt alaposztálya, amelyből Employee és Customer származtatott osztályokból áll. A polimorf deszerializálás azt jelenti, hogy a tervezéskor megadható Person deszerializálási célként, a CustomerEmployee JSON objektumai pedig futásidőben megfelelően deszerializálódnak. A deszerializálás során olyan nyomokat kell találnia, amelyek azonosítják a szükséges típust a JSON-ban. A rendelkezésre álló nyomok típusai az egyes forgatókönyvekben eltérőek. Előfordulhat például, hogy egy diszkriminatív tulajdonság elérhető, vagy egy adott tulajdonság jelenlétére vagy hiányára kell támaszkodnia. Az aktuális kiadás System.Text.Json nem biztosít attribútumokat a polimorf deszerializálási forgatókönyvek kezeléséhez, ezért egyéni konverterekre van szükség.

Az alábbi kód egy alaposztályt, két származtatott osztályt és egy egyéni konvertert jelenít meg számukra. A konverter diszkriminatív tulajdonságot használ a polimorf deszerializáláshoz. A típuskriminatív nem szerepel az osztálydefiníciókban, de szerializáláskor jön létre, és a deszerializálás során olvassa be.

Fontos

A példakód megköveteli, hogy a JSON-objektumnév/érték párok sorrendben maradjanak, ami nem a JSON szabványkövetelménye.

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

A következő kód regisztrálja a konvertert:

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

A konverter képes deszerializálni az azonos konverterrel létrehozott JSON-t a szerializáláshoz, például:

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

Az előző példában szereplő konverterkód manuálisan olvassa és írja be az egyes tulajdonságokat. Másik lehetőségként meghívhatja Deserialize vagy Serialize elvégezheti a munka egy részét. Például tekintse meg ezt a StackOverflow-bejegyzést.

Alternatív módszer a polimorf deszerializálásra

A metódusban Read a következőt hívhatja Deserialize meg:

  • Klónozza a példányt Utf8JsonReader . Mivel Utf8JsonReader ez egy szerkezet, ez csak egy hozzárendelési utasítást igényel.
  • A klón használatával olvassa át a diszkriminatív jogkivonatokat.
  • Hívja meg Deserialize az eredeti Reader példányt, ha már ismeri a kívánt típust. Hívhat, Deserialize mert az eredeti Reader példány továbbra is a kezdő objektum jogkivonatának olvasásához van elhelyezve.

Ennek a módszernek a hátránya, hogy nem adhatja meg az eredeti beállításpéldányt, amely a konvertert Deserializeregisztrálja. Ez egy verem túlcsordulását okozhatja, ahogyan azt a Szükséges tulajdonságok című témakörben ismertetik. Az alábbi példa egy Read olyan módszert mutat be, amely ezt a alternatívát használja:

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

Típusok esetén az Stack oda-vissza utazás támogatása

Ha egy JSON-sztringet deszerializál egy Stack objektumba, majd szerializálja azt, a verem tartalma fordított sorrendben van. Ez a viselkedés a következő típusokra és felületekre, valamint az ezekből származtatott felhasználó által definiált típusokra vonatkozik:

Az eredeti sorrendet a veremben megőrző szerializálás és deszerializálás támogatásához egyéni konverterre van szükség.

Az alábbi kód egy egyéni konvertert mutat be, amely lehetővé teszi az objektumokra és az objektumokról való Stack<T> ciklikus bemásolást:

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

A következő kód regisztrálja a konvertert:

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

Elnevezési szabályzatok enumerializáláshoz

Alapértelmezés szerint a beépített függvény JsonStringEnumConverter szerializálhatja és deszerializálhatja az enumerálás sztringértékeit. Egy megadott elnevezési szabályzat vagy az CamelCase elnevezési szabályzat nélkül működik. Nem támogatja az egyéb elnevezési szabályzatokat, például a kígyó esetét. Az egyéni konverterkóddal kapcsolatos információkért, amelyek támogatják az enumerálási sztringértékek közötti és az azokból való ciklikus be- és visszacsatolást egy kígyó-esetelnevezési szabályzat használatakor, tekintse meg a GitHub dotnet/runtime #31619 problémáját. Másik lehetőségként frissítsen a .NET 7 vagy újabb verzióira, amelyek beépített támogatást nyújtanak az elnevezési szabályzatok alkalmazásához az enumerálási sztringértékekre való ciklikus be- és visszacsatoláskor.

Alapértelmezett rendszerkonverter használata

Bizonyos esetekben érdemes lehet az alapértelmezett rendszerkonvertert használni egy egyéni konverterben. Ehhez kérje le a rendszerkonvertert a JsonSerializerOptions.Default tulajdonságból az alábbi példában látható módon:

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

A null értékek kezelése

Alapértelmezés szerint a szerializáló a következőképpen kezeli a null értékeket:

  • Referenciatípusok és Nullable<T> típusok esetén:

    • A szerializálás nem továbbítja null az egyéni konvertereket.
    • Nem adja át JsonTokenType.Null az egyéni konvertereknek a deszerializálást.
    • Deszerializálási példányt null ad vissza.
    • Közvetlenül az íróval szerializál null .
  • Nem null értékű értéktípusok esetén:

    • A deszerializálás egyéni konvertereinek továbbítja JsonTokenType.Null . (Ha nem érhető el egyéni konverter, a típus belső konvertere kivételt JsonException jelez.)

Ez a nullkezelési viselkedés elsősorban a teljesítményt optimalizálja, ha kihagy egy extra hívást a konverternek. Emellett elkerüli a null értékű típusok konvertereinek kényszerítését, hogy minden Read egyes metódus Write felülbírálásakor ellenőrizze null azokat.

Ha engedélyezni szeretné, hogy egy egyéni konverter egy referencia- vagy értéktípust kezeljen null , felülbírálást JsonConverter<T>.HandleNull kell visszaadnia true, ahogyan az alábbi példában látható:

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.

Hivatkozások megőrzése

Alapértelmezés szerint a referenciaadatok csak az egyes hívásokhoz vagy hívásokhoz gyorsítótárazva lesznek SerializeDeserialize. Ha meg szeretné őrizni a hivatkozásokat az egyik Serialize/Deserialize hívásból a másikba, gyökereztetheti a ReferenceResolver példányt a híváswebhelyen.Serialize/Deserialize Az alábbi kód egy példát mutat be erre a forgatókönyvre:

  • A típushoz Company egyéni konvertert kell írnia.
  • Nem szeretné manuálisan szerializálni a Supervisor tulajdonságot, amely egy Employee. Ezt a szerializálónak szeretné delegálni, és meg szeretné őrizni a már mentett hivatkozásokat is.

Íme az osztályok és Company az Employee osztályok:

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

A konverter a következőképpen néz ki:

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

Egy olyan osztály, amely a hivatkozások szótárban való tárolásából ReferenceResolver származik:

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

Egy osztály, amely egy példányból ReferenceHandlerMyReferenceResolver származik, és csak szükség esetén hoz létre új példányt (az ebben a példában elnevezett Reset metódusban):

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

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

Amikor a mintakód meghívja a szerializálót, egy olyan példányt JsonSerializerOptions használ, amelyben a ReferenceHandler tulajdonság egy példányra MyReferenceHandlervan állítva. Ha követi ezt a mintát, mindenképpen állítsa alaphelyzetbe a szótárat, amikor végzett a ReferenceResolver szerializálással, hogy ne nőjön örökké.

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

Az előző példa csak szerializálást végez, de hasonló megközelítés alkalmazható a deszerializáláshoz.

Egyéb egyéni konverterminták

A Migrálás a cikkből Newtonsoft.JsonSystem.Text.Json az egyéni konverterek további mintáit tartalmazza.

A forráskód egységtesztelési mappájaSystem.Text.Json.Serialization más egyéni konvertermintákat is tartalmaz, például:

Ha olyan konvertert kell készítenie, amely módosítja egy meglévő beépített konverter viselkedését, lekérheti a meglévő konverter forráskódját, hogy kiindulópontként szolgáljon a testreszabáshoz.

További erőforrások