.NET'te JSON serileştirme (marshalling) için özel dönüştürücüler yazma

Bu makalede, ad alanında System.Text.Json sağlanan JSON serileştirme sınıfları için özel dönüştürücülerin nasıl oluşturulacağı gösterilmektedir. giriş için System.Text.Jsonbkz . .NET'te JSON'ı seri hale getirme ve seri durumdan çıkarma.

Dönüştürücü, bir nesneyi veya değeri JSON'a ve JSON'dan dönüştüren bir sınıftır. Ad System.Text.Json alanında, JavaScript ilkelleriyle eşlenen çoğu temel tür için yerleşik dönüştürücüler bulunur. Yerleşik dönüştürücülerin varsayılan davranışını geçersiz kılmak için özel dönüştürücüler yazabilirsiniz. Örneğin:

  • Değerlerin aa/gg/yyyy biçiminde gösterilmesini isteyebilirsiniz DateTime . Varsayılan olarak, RFC 3339 profili de dahil olmak üzere ISO 8601-1:2019 desteklenir. Daha fazla bilgi için, içindeki DateTime ve DateTimeOffset desteğine System.Text.Jsonbakın.
  • PoCO'ları JSON dizesi olarak, örneğin bir PhoneNumber türle seri hale getirmek isteyebilirsiniz.

Ayrıca, yeni işlevlerle özelleştirmek veya genişletmek System.Text.Json için özel dönüştürücüler yazabilirsiniz. Bu makalenin devamında aşağıdaki senaryolar ele alınmıştır:

Visual Basic, özel dönüştürücüler yazmak için kullanılamaz, ancak C# kitaplıklarında uygulanan dönüştürücüleri çağırabilir. Daha fazla bilgi için bkz . Visual Basic desteği.

Özel dönüştürücü desenleri

Özel dönüştürücü oluşturmak için iki desen vardır: temel desen ve fabrika deseni. Fabrika düzeni, tür veya açık genel türleri Enum işleyen dönüştürücüler içindir. Temel desen, genel olmayan ve kapalı genel türler içindir. Örneğin, aşağıdaki türler için dönüştürücüler fabrika desenini gerektirir:

Temel desen tarafından işlenebilen bazı tür örnekleri şunlardır:

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

Temel desen, bir türü işleyebilen bir sınıf oluşturur. Fabrika düzeni, çalışma zamanında hangi türün gerekli olduğunu belirleyen ve uygun dönüştürücüleri dinamik olarak oluşturan bir sınıf oluşturur.

Örnek temel dönüştürücü

Aşağıdaki örnek, mevcut bir veri türü için varsayılan serileştirmeyi geçersiz kılan bir dönüştürücüdür. Dönüştürücü, özellikler için DateTimeOffset aa/gg/yy biçimini kullanır.

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

Örnek fabrika desen dönüştürücüsü

Aşağıdaki kod ile Dictionary<Enum,TValue>çalışan özel bir dönüştürücü gösterir. İlk genel tür parametresi Enum ve ikincisi açık olduğundan kod fabrika desenini izler. CanConvert yöntemi yalnızca Dictionary ilki bir tür olan iki genel parametresi olan için Enum döndürürtrue. İç dönüştürücü, için çalışma zamanında sağlanan türü işlemek için TValuemevcut bir dönüştürücü alır.

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

Temel deseni izleme adımları

Aşağıdaki adımlarda, temel deseni izleyerek dönüştürücü oluşturma adımları açıklanmaktadır:

  • burada serileştirilecek ve seri durumdan çıkarılacak tür olan bir JsonConverter<T>T sınıf oluşturun.
  • Read Gelen JSON'un seri durumdan çıkarılıp türüne Tdönüştürmek için yöntemini geçersiz kılın. Utf8JsonReader JSON okumak için yöntemine geçirilen öğesini kullanın. Seri hale getirici geçerli JSON kapsamı için tüm verileri geçirdiğinden kısmi verileri işleme konusunda endişelenmeniz gerekmez. Bu nedenle, öğesinin döndürdüğünü trueRead doğrulamak için veya TrySkip çağrısı Skip yapmak gerekmez.
  • türündeki WriteTgelen nesnesini seri hale getirmek için yöntemini geçersiz kılın. Utf8JsonWriter JSON yazmak için yöntemine geçirilen öğesini kullanın.
  • CanConvert Yalnızca gerekirse yöntemini geçersiz kılın. Varsayılan uygulama, dönüştürülecek tür türünde Tolduğunda döndürürtrue. Bu nedenle, yalnızca türü T destekleyen dönüştürücülerin bu yöntemi geçersiz kılması gerekmez. Bu yöntemi geçersiz kılması gereken bir dönüştürücü örneği için bu makalenin devamında yer alan polimorfik seri durumdan çıkarma bölümüne bakın.

Özel dönüştürücüler yazmak için başvuru uygulamaları olarak yerleşik dönüştürücüler kaynak koduna başvurabilirsiniz.

Fabrika desenini izleme adımları

Aşağıdaki adımlarda fabrika desenini izleyerek dönüştürücü oluşturma adımları açıklanmaktadır:

  • öğesinden JsonConverterFactorytüretilen bir sınıf oluşturun.
  • CanConvert Dönüştürülecek tür dönüştürücü tarafından işlenebilir olduğunda döndürülecek true yöntemi geçersiz kılın. Örneğin, dönüştürücü içinseList<T>, yalnızca , List<string>ve List<DateTime>işleyebilirList<int>.
  • CreateConverter Çalışma zamanında sağlanan tür dönüştürme işlemini işleyecek bir dönüştürücü sınıfının örneğini döndürmek için yöntemini geçersiz kılın.
  • Yöntemin örnek oluşturduğu CreateConverter dönüştürücü sınıfını oluşturun.

Bir nesneyi dizeye ve dizeden dönüştürme kodu tüm türler için aynı olmadığından, açık genel türler için fabrika düzeni gereklidir. Açık bir genel tür için dönüştürücü (List<T>örneğin), arka planda kapalı bir genel tür (List<DateTime>örneğin) için bir dönüştürücü oluşturması gerekir. Dönüştürücü tarafından işlenebilen her kapalı genel türü işlemek için kod yazılmalıdır.

Tür Enum , açık bir genel türe benzer: için Enum dönüştürücü, arka planda belirli Enum bir (WeekdaysEnumörneğin) için bir dönüştürücü oluşturması gerekir.

yönteminde Read kullanımı Utf8JsonReader

Dönüştürücünüz bir JSON nesnesini dönüştürüyorsa, Utf8JsonReader yöntemi başladığında begin nesnesi belirtecinde Read konumlandırılır. Daha sonra bu nesnedeki tüm belirteçleri okumanız ve okuyucunun ilgili uç nesne belirtecinde konumlandırılmış şekilde yöntemden çıkmanız gerekir. Nesnenin sonunun ötesini okursanız veya karşılık gelen uç belirteci ulaşmadan önce durdurursanız şunu belirten bir JsonException özel durum alırsınız:

'ConverterName' dönüştürücüsü çok fazla okuyor veya yeterli değil.

Bir örnek için önceki fabrika deseni örnek dönüştürücüsne bakın. yöntemi, Read okuyucunun bir başlangıç nesnesi belirtecinde konumlandırıldığını doğrulayarak başlar. Sonraki uç nesne belirtecinde konumlandırıldığını bulana kadar okur. Nesnenin içindeki bir nesneyi gösterecek bir araya gelen başlatma nesnesi belirteci olmadığından sonraki uç nesne belirtecinde durur. Bir diziyi dönüştürüyorsanız başlangıç belirteci ve bitiş belirteci ile ilgili aynı kural geçerlidir. Örnek için bu makalenin devamında yer alan Stack<T> örnek dönüştürücüye bakın.

Hata işleme

Seri hale getirici özel durum türleri JsonException ve NotSupportedExceptioniçin özel işleme sağlar.

JsonException

İleti olmadan bir JsonException oluşturursanız, seri hale getirici hataya neden olan JSON bölümünün yolunu içeren bir ileti oluşturur. Örneğin, deyimi throw new JsonException() aşağıdaki örneğe benzer bir hata iletisi oluşturur:

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

Bir ileti (örneğin, ) sağlarsanız, throw new JsonException("Error occurred")seri hale getirici yine de , LineNumberve BytePositionInLine özelliklerini ayarlarPath.

Notsupportedexception

oluşturursanız NotSupportedException, her zaman iletideki yol bilgilerini alırsınız. Bir ileti sağlarsanız, yol bilgileri iletiye eklenir. Örneğin, deyimi throw new NotSupportedException("Error occurred.") aşağıdaki örneğe benzer bir hata iletisi oluşturur:

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

Hangi özel durum türü ne zaman oluşturulur?

JSON yükü seri durumdan çıkarılmakta olan tür için geçerli olmayan belirteçler içerdiğinde bir JsonExceptionoluşturun.

Belirli türlere izin vermek istemediğinizde, bir NotSupportedExceptionatabilirsiniz. Bu özel durum, seri hale getiricinin desteklenmeyen türler için otomatik olarak oluşturduğunu gösterir. Örneğin, System.Type güvenlik nedeniyle desteklenmez, bu nedenle seri durumdan çıkarma girişimi bir NotSupportedExceptionile sonuçlanır.

Gerektiğinde başka özel durumlar da oluşturabilirsiniz, ancak bunlar otomatik olarak JSON yol bilgilerini içermez.

Özel dönüştürücü kaydetme

ve Deserialize yöntemlerinin Serialize kullanmasını sağlamak için özel bir dönüştürücü kaydedin. Aşağıdaki yaklaşımlardan birini seçin:

Kayıt örneği - Dönüştürücüler koleksiyonu

DateTimeOffsetJsonConverter'ın türündeki özellikler için varsayılan olmasını sağlayan bir örnek aşağıda verilmiştirDateTimeOffset:

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

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Aşağıdaki türdeki bir örneği seri hale getirdiğinizden şu şekilde düşünebilirsiniz:

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

Özel dönüştürücüsünün kullanıldığını gösteren bir JSON çıktısı örneği aşağıda verilmişti:

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

Aşağıdaki kod, özel DateTimeOffset dönüştürücü kullanarak seri durumdan çıkarmak için aynı yaklaşımı kullanır:

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

Bir özellik üzerinde kayıt örneği - [JsonConverter]

Aşağıdaki kod özelliği için Date özel bir dönüştürücü seçer:

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

Seri hale WeatherForecastWithConverterAttribute getirmek için kullanılacak kodun kullanılması JsonSerializeOptions.Convertersgerekmez:

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

Seri durumdan çıkarma kodu için de kullanılması Convertersgerekmez:

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

Kayıt örneği - bir tür üzerinde [JsonConverter]

Bir yapı oluşturan ve özniteliğini [JsonConverter] uygulayan kod aşağıdadır:

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

Yukarıdaki yapı için özel dönüştürücü aşağıdadır:

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

[JsonConverter] yapısındaki özniteliği, türündeki Temperatureözellikler için varsayılan olarak özel dönüştürücü kaydeder. Dönüştürücü, seri hale getirdiğinizde veya seri durumdan çıkardığınızda aşağıdaki türün özelliğinde TemperatureCelsius otomatik olarak kullanılır:

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

Dönüştürücü kaydı önceliği

Serileştirme veya seri durumdan çıkarma sırasında, her JSON öğesi için en yüksek önceliğe ve en düşük önceliğe kadar listelenen aşağıdaki sırayla bir dönüştürücü seçilir:

  • [JsonConverter] bir özelliğe uygulanır.
  • Koleksiyona Converters bir dönüştürücü eklendi.
  • [JsonConverter] özel bir değer türüne veya POCO'ya uygulanır.

Koleksiyonda Converters bir tür için birden çok özel dönüştürücü kayıtlıysa, döndüren trueCanConvert ilk dönüştürücü kullanılır.

Yerleşik dönüştürücü yalnızca geçerli bir özel dönüştürücü kaydedilmediyse seçilir.

Yaygın senaryolar için dönüştürücü örnekleri

Aşağıdaki bölümlerde, yerleşik işlevselliğin işlemediği bazı yaygın senaryoları ele alan dönüştürücü örnekleri sağlanır.

Örnek DataTable dönüştürücü için bkz . Desteklenen koleksiyon türleri.

Çıkarsanan türleri nesne özelliklerine seri durumdan çıkarma

türünde objectbir özelliğe seri durumdan çıkarılırken bir JsonElement nesne oluşturulur. Bunun nedeni, seri durumdan çıkarıcının hangi CLR türünü oluşturacağını bilmemesi ve tahmin etmeye çalışmamasıdır. Örneğin, bir JSON özelliğinde "true" varsa, seri durumdan çıkarıcı değerin bir Booleanolduğunu çıkarmaz ve bir öğenin "01/01/2019" değeri varsa seri durumdan çıkarıcı bunun bir DateTimeolduğunu çıkarmaz.

Tür çıkarımı yanlış olabilir. Seri durumdan çıkarıcı ondalık ayırıcısı olmayan bir JSON sayısını olarak longayrıştırıyorsa, değer ilk olarak veya ulongBigIntegerolarak serileştirilmişse aralık dışı sorunlara neden olabilir. Ondalık ayırıcısı double olan bir sayıyı olarak ayrıştırma, sayı başlangıçta olarak decimalserileştirilmişse duyarlığı kaybedebilir.

Tür çıkarımı gerektiren senaryolar için, aşağıdaki kod özellikler için object özel bir dönüştürücü gösterir. Kod şu işlemleri dönüştürür:

  • trueve falseBoolean
  • Ondalık olmayan sayılar long
  • Ondalık içeren sayılar: double
  • Tarihler: DateTime
  • Dizeler: string
  • Diğer her şey 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"
//}

Örnekte dönüştürücü kodu ve özellikleri olan object bir WeatherForecast sınıf gösterilmektedir. Main yöntemi, bir JSON dizesini önce dönüştürücü kullanmadan ve sonra dönüştürücüsü kullanmadan bir WeatherForecast örneğe seri durumdan çıkartır. Konsol çıkışı, dönüştürücü olmadan özelliğin DateJsonElementçalışma zamanı türünün ; dönüştürücü ile çalışma zamanı türünün olduğunu DateTimegösterir.

Ad alanı içindeki birim testleri klasöründe, özelliklere System.Text.Json.Serialization seri durumdan çıkarma object işlemini işleyen özel dönüştürücülere daha fazla örnek bulunur.

Polimorfik seri durumdan çıkarma desteği

.NET 7, hem polimorfik serileştirme hem de seri durumdan çıkarma desteği sağlar. Ancak, önceki .NET sürümlerinde sınırlı polimorfik serileştirme desteği vardı ve seri durumdan çıkarma desteği yoktu. .NET 6 veya önceki bir sürümü kullanıyorsanız seri durumdan çıkarma için özel bir dönüştürücü gerekir.

Örneğin, ve Customer türetilmiş sınıflarla Employee soyut bir Person temel sınıfınız olduğunu varsayalım. Polimorfik seri durumdan çıkarma, tasarım zamanında seri durumdan çıkarma hedefi olarak belirtebileceğiniz Person ve CustomerEmployee JSON'daki nesnelerin çalışma zamanında doğru seri durumdan çıkarıldığı anlamına gelir. Seri durumdan çıkarma sırasında, JSON'da gerekli türü tanımlayan ipuçları bulmanız gerekir. Kullanılabilir ipucu türleri her senaryoya göre farklılık gösterir. Örneğin, ayrımcı özelliği kullanılabilir olabilir veya belirli bir özelliğin varlığına veya yokluğuna güvenmeniz gerekebilir. geçerli sürümü System.Text.Json , çok biçimli seri durumdan çıkarma senaryolarının nasıl işleneceğini belirten öznitelikler sağlamaz, bu nedenle özel dönüştürücüler gereklidir.

Aşağıdaki kodda bir temel sınıf, iki türetilmiş sınıf ve bunlar için özel bir dönüştürücü gösterilmektedir. Dönüştürücü, polimorfik seri durumdan çıkarma yapmak için ayrıştırıcı özelliği kullanır. Tür ayrımcısı sınıf tanımlarında değil, serileştirme sırasında oluşturulur ve seri durumdan çıkarma sırasında okunur.

Önemli

Örnek kod, JSON nesne adı/değer çiftlerinin sıralı kalmasını gerektirir; bu JSON'un standart bir gereksinimi değildir.

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şağıdaki kod dönüştürücüye kaydeder:

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

Dönüştürücü, seri hale getirmek için aynı dönüştürücü kullanılarak oluşturulan JSON'un seri durumdan çıkarılabilir, örneğin:

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

Önceki örnekteki dönüştürücü kodu her özelliği el ile okur ve yazar. Bunun alternatifi, işin bir kısmını aramak Deserialize veya Serialize yapmaktır. Bir örnek için bu StackOverflow gönderisini inceleyin.

Polimorfik seri durumdan çıkarmanın alternatif bir yolu

yöntemini çağırabilirsiniz DeserializeRead :

  • Örneğin bir kopyasını Utf8JsonReader oluşturun. Utf8JsonReader Bir yapı olduğundan, bunun için yalnızca bir atama deyimi gerekir.
  • Ayırıcı belirteçleri okumak için kopyayı kullanın.
  • İhtiyacınız olan türü bildiğinizde özgün Reader örneği kullanarak arayınDeserialize. Özgün Reader örnek hala begin nesne belirtecini okuyacak şekilde konumlandırıldığından çağırabilirsinizDeserialize.

Bu yöntemin bir dezavantajı, dönüştürücüye kaydeden özgün seçenekler örneğini geçirememenizdir Deserialize. Bunun yapılması, Gerekli özellikler bölümünde açıklandığı gibi yığın taşmasına neden olur. Aşağıdaki örnekte bu alternatifi kullanan bir Read yöntem gösterilmektedir:

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ürler için Stack gidiş dönüş desteği

Bir JSON dizesini bir Stack nesneye seri durumdan çıkartır ve sonra bu nesneyi serileştirirseniz, yığının içeriği ters sırada olur. Bu davranış, aşağıdaki türler ve arabirimler ile bunlardan türetilen kullanıcı tanımlı türler için geçerlidir:

Yığındaki özgün sırayı koruyan serileştirmeyi ve seri durumdan çıkarmayı desteklemek için özel bir dönüştürücü gereklidir.

Aşağıdaki kod, nesnelere ve nesnelerden Stack<T> yuvarlak kopyalamayı etkinleştiren özel bir dönüştürücü gösterir:

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şağıdaki kod dönüştürücüye kaydeder:

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

Numaralandırma dizesi seri durumdan çıkarma için adlandırma ilkeleri

Varsayılan olarak, yerleşik JsonStringEnumConverter sabit listeleri için dize değerlerini seri hale getirebilir ve seri durumdan çıkarabilirsiniz. Belirtilen adlandırma ilkesi olmadan veya adlandırma ilkesiyle CamelCase çalışır. Yılan örneği gibi diğer adlandırma ilkelerini desteklemez. Yılan örneği adlandırma ilkesi kullanılırken sabit listesi dizesi değerlerine gidiş dönüşlü kopyalamayı destekleyebilecek özel dönüştürücü kodu hakkında bilgi için bkz. GitHub sorunu dotnet/runtime #31619. Alternatif olarak, sabit listesi dizesi değerlerine gidiş-dönüş yaparken adlandırma ilkelerini uygulamak için yerleşik destek sağlayan .NET 7 veya sonraki sürümlerine yükseltin.

Varsayılan sistem dönüştürücüsü kullan

Bazı senaryolarda, özel bir dönüştürücüde varsayılan sistem dönüştürücüsü kullanmak isteyebilirsiniz. Bunu yapmak için, aşağıdaki örnekte gösterildiği gibi özelliğinden sistem dönüştürücüsünü JsonSerializerOptions.Default alın:

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

Null değerleri işleme

Seri hale getirici varsayılan olarak null değerleri aşağıdaki gibi işler:

  • Başvuru türleri ve Nullable<T> türleri için:

    • Serileştirmede özel dönüştürücülere geçirilmez null .
    • Seri durumdan çıkarmada özel dönüştürücülere geçmez JsonTokenType.Null .
    • Seri durumdan çıkarmada bir null örnek döndürür.
    • Serileştirmede doğrudan yazıcıyla birlikte yazar null .
  • Null değer atanamayan değer türleri için:

    • Seri durumdan çıkarma işleminde özel dönüştürücülere geçer JsonTokenType.Null . (Kullanılabilir özel dönüştürücü yoksa, türü için iç dönüştürücü tarafından bir JsonException özel durum oluşturulur.)

Bu null işleme davranışı öncelikle dönüştürücüye ek çağrı atlayarak performansı iyileştirmektir. Buna ek olarak, dönüştürücüleri null atanabilir türler için her Read ve Write yöntem geçersiz kılma işleminin başında denetlenmeye null zorlamaktan kaçınılır.

Bir başvuru veya değer türü için işlemek null üzere özel dönüştürücü etkinleştirmek için, aşağıdaki örnekte gösterildiği gibi döndürecek trueşekilde geçersiz kılınJsonConverter<T>.HandleNull:

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.

Başvuruları koruma

Varsayılan olarak, başvuru verileri yalnızca veya çağrısı için önbelleğe SerializeDeserializealınır. Bir çağrıdan diğerine Serialize/Deserialize yapılan başvuruları kalıcı hale getirmek için, çağrısı sitesindeki örneğin kökünü/ReferenceResolverSerializeDeserializeoluşturun. Aşağıdaki kodda bu senaryo için bir örnek gösterilmektedir:

  • Türü için Company özel bir dönüştürücü yazarsınız.
  • özelliği el ile seri hale Supervisor getirmek istemezsiniz. Bu bir Employee. Bunu seri hale getiriciye devretmek ve ayrıca önceden kaydettiğiniz başvuruları korumak istiyorsunuz.

ve Company sınıfları şunlardırEmployee:

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

Dönüştürücü şöyle görünür:

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

Başvurulardan ReferenceResolver türetilen bir sınıf, başvuruları bir sözlükte depolar:

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

öğesinden ReferenceHandler türetilen bir sınıf, örneğini MyReferenceResolver barındırıyor ve yalnızca gerektiğinde yeni bir örnek oluşturuyor (bu örnekte adlı Reset bir yöntemde):

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

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

Örnek kod seri hale getiriciyi çağırdığında, özelliğinin ReferenceHandler bir JsonSerializerOptions örneğine ayarlandığı bir örneği MyReferenceHandlerkullanır. Bu düzeni uyguladığınızda, serileştirmeyi bitirdiğinizde sözlüğü sıfırladığınızdan ReferenceResolver emin olun ve sonsuza kadar büyümesini engelleyebilirsiniz.

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

Yukarıdaki örnek yalnızca serileştirme yapar, ancak seri durumdan çıkarma için benzer bir yaklaşım benimsenebilir.

Diğer özel dönüştürücü örnekleri

Geçiş kaynağı Newtonsoft.JsonSystem.Text.Json makalesi ek özel dönüştürücü örnekleri içerir.

Kaynak kodundaki System.Text.Json.Serialization birim testleri klasörü, aşağıdakiler gibi diğer özel dönüştürücü örneklerini içerir:

Mevcut yerleşik dönüştürücülerin davranışını değiştiren bir dönüştürücü yapmanız gerekiyorsa, özelleştirme için bir başlangıç noktası olarak hizmet vermek üzere var olan dönüştürücüye ait kaynak kodunu alabilirsiniz.

Ek kaynaklar