.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. Bir System.Text.Json tanıtımı için, .NET'te JSON'ı serileştirme ve seriden çıkarma başlığına bakınız.

Dönüştürücü, bir nesneyi veya değeri JSON'a ve JSON'dan dönüştüren bir sınıftır. System.Text.Json ad alanı, JavaScript ilkellerine eşlenen çoğu temel tür için yerleşik dönüştürücüler içerir. 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 deseni, tür Enum veya açık genel türleri 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, ilki bir Enum türü olan iki genel parametresi bulunan bir Dictionary için yalnızca true döndürür. İç dönüştürücü, TValue için çalışma zamanında sağlanan herhangi bir türü işlemek üzere mevcut bir dönüştürücüyü 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:

  • Bir JsonConverter<T> sınıfından türeyen ve T türünün serileştirilip seri durumdan çıkarılacağı bir 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. Yönteme geçirilen Utf8JsonReader parametresini kullanarak JSON'u okuyun. 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 Skip veya TrySkip çağrısı yapmaya ya da Read'nin true döndürdüğünü doğrulamaya gerek yoktur.
  • Yöntemini geçersiz kılın ve T türündeki gelen nesneyi seri hale getirin. Utf8JsonWriter yöntemine geçirilen parametreyi JSON yazmak için kullanın.
  • Yöntemi yalnızca gerekirse CanConvert ile geçersiz kılın. Dönüştürülecek tür T türünde olduğunda, varsayılan uygulama true döndürür. 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.
  • Dönüştürülecek tür, dönüştürücü tarafından işlenebilir olduğunda CanConvert yöntemini true döndürmek için geçersiz kılın. Örneğin, dönüştürücü List<T> içinse, yalnızca List<int>, List<string> ve List<DateTime> işleyebilir.
  • CreateConverter yöntemini, çalışma zamanında sağlanan dönüştürülecek türü işleyecek bir dönüştürücü sınıfının örneğini döndürecek şekilde 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: Enum için bir dönüştürücünün, arka planda belirli bir Enum (örneğin WeekdaysEnum) için bir dönüştürücü oluşturması gerekir.

Utf8JsonReader yönteminde Read kullanımı

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 veya yetersiz veri okuma işlemi yapıyor.

Örnek görmek için önceki fabrika tasarımı örneği dönüştürücüsüne bakın. Başlangıçta Read yöntemi, okuyucunun başlangıç nesnesi belirtecinde konumlandığını doğrular. Sonraki uç nesne belirtecinde konumlandırıldığını bulana kadar okur. Arada kalan ve nesnenin içinde başka bir nesneyi gösterecek başlangıç 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 yönetimi

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, throw new JsonException("Error occurred")) sağlarsanız, serileştirici yine de Path, LineNumber ve BytePositionInLine özelliklerini ayarlar.

NotSupportedException

Eğer bir NotSupportedException atarsanız, 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 atılır?

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 fırlattığı bir istisnadır. Örneğin, System.Type güvenlik nedeniyle desteklenmez, bu nedenle serileştirme girişimi bir NotSupportedException ile 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

Özel bir dönüştürücü kaydedin, böylece Serialize ve Deserialize yöntemleri bunu kullanabilir. 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
};
serializeOptions.Converters.Add(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 getirmek için WeatherForecastWithConverterAttribute kodunun JsonSerializeOptions.Converters kullanılması gerekmez:

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

Seri çözme kodu için Converters kullanılmasına gerek yoktur.

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

Kayıt örneği - bir tür için [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ü, aşağıdaki türün TemperatureCelsius özelliğinde serileştirildiğinde veya seriden çıkarıldığında 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 bir tür için birden fazla özel dönüştürücü kayıtlıysa, ilk olarak CanConvert için true döndüren 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üsü için bkz. Desteklenen türler.

Çıkarsanan türleri nesne özelliklerine deserileştirme

object türünde bir ö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 nokta içermeyen bir JSON sayısını long olarak ayıklarsa, değer başta ulong veya BigInteger olarak serileştirilmişse bu, aralık dışı sorunlara neden olabilir. Ondalık ayırıcısı double olan bir sayıyı ayrıştırma, sayı başlangıçta decimal olarak serileştirilmişse kesinliği 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:

  • true ve falseBoolean
  • Ondalık olmayan sayılar long'e
  • Ondalık içeren sayılar: double
  • Tarihler: DateTime
  • Dizeler: string
  • Her şey JsonElement dışındaki diğer her şey
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)
        {
            var runtimeType = objectToWrite.GetType();
            if (runtimeType == typeof(object))
            {
                writer.WriteStartObject();
                writer.WriteEndObject();
                return;
            }

            JsonSerializer.Serialize(writer, objectToWrite, runtimeType, 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 Run()
        {
            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 object özellikleri olan bir WeatherForecast sınıfı gösterilmektedir. Main yöntemi, bir JSON dizesini önce dönüştürücü kullanmadan, ardından dönüştürücüyü kullanarak bir WeatherForecast örneğe seri durumdan çıkartır. Konsol çıktısı, dönüştürücü olmadan Date özelliğinin çalışma zamanı türünün JsonElement olduğunu; dönüştürücü ile çalışma zamanı türünün DateTime olduğunu gösterir.

Namespace alanındaki birim testleri klasöründe, özelliklere seri durumdan çıkarma işlemini işleyen özel dönüştürücülere dair daha fazla örnek bulunmaktadır.

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 deseriyalizasyon için özel bir dönüştürücü gerekir.

Örneğin, Person soyut bir temel sınıfınız ve Employee ile Customer türetilmiş sınıfları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. Serileştirmeden çıkartma sırasında, JSON'da gerekli türü tanımlamak için 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ğlamadığından, ö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.

Important

Ö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üyü kaydeder:

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

Dönüştürücü, örneğin aynı dönüştürücü kullanılarak seri hale getirilmiş JSON'u seri durumdan çıkartabilir.

[
  {
    "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 yerine, işin bir kısmını yapmak için Deserialize veya Serialize çağrılabilir. Bir örnek için bu StackOverflow gönderisini inceleyin.

Polimorfik serileştirmeden çıkarmanın alternatif bir yolu

Deserialize yönteminde Read çağırabilirsiniz.

  • Utf8JsonReader örneğinin bir kopyasını 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ğini Deserialize kullanarak arayın. Deserialize çağırabilirsiniz çünkü özgün Reader örneği hala başlatma nesne belirtecini okuyacak şekilde konumlandırılmış durumda.

Bu yöntemin bir dezavantajı, dönüştürücüyü kaydeden özgün seçenekler örneğini Deserialize'a geçirememenizdir. 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 dizisini bir Stack nesnesine serisini çözüp tekrar serileştirirseniz, yığın içeriği ters sırada olacaktır. 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, Stack<T> objelere ve objelerden çift yönlü dönüşümü etkinleştiren özel bir çevirici 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üyü kaydeder:

var options = new JsonSerializerOptions();
options.Converters.Add(new JsonConverterFactoryForStackOfT());

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, JsonSerializerOptions.Default özelliğinden sistem dönüştürücüsünü 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:

  • Referans türleri ve Nullable<T> türleri için:

    • Özel dönüştürücülere null serileştirme sırasında iletilmez.
    • Seri durumdan çıkarma işleminde JsonTokenType.Null özel dönüştürücülere geçmez.
    • Seri durumdan çıkarma işlemi sırasında bir null örneği döndürür.
    • Serileştirme sırasında null doğrudan bir yazıcı ile yazar.
  • Null değer atanamayan değer türleri için:

    • Seri durumdan çıkarma işleminde JsonTokenType.Null öğesini özel dönüştürücülere aktarır. (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ülerin null atanabilir türlerle ilgili her null ve Read geçersiz kılma işleminin başında Write kontrol edilmesi zorunluluğundan kaçınılır.

Özel bir dönüştürücüyü, bir başvuru veya değer türünü ele almak üzere etkinleştirmek için JsonConverter<T>.HandleNull yöntemini geçersiz kılın ve true döndürün. Aşağıdaki örnekte olduğu gibi:

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 Run()
        {
            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.

Referansları koru

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

  • Company türü için özel bir dönüştürücü yazıyorsunuz.
  • Özelliği el ile Supervisor olarak seri hale getirmek istemezsiniz. Bu bir Employee. Bunu serileştiriciye devretmek ve ayrıca önceden kaydettiğiniz referansları korumak istiyorsunuz.

İşte Employee ve Company sınıfları:

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

ReferenceResolver sınıfından türetilen bir sınıf, referansları 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 getirici çağırdığı zaman, JsonSerializerOptions özelliği bir ReferenceHandler örneğine ayarlanmış olan MyReferenceHandler örneğini kullanı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 deserialize etmek için benzer bir yaklaşım benimsenebilir.

Özel dönüştürücülerde ReferenceResolver sınırlamaları

Preserve kullandığınızda, seri hale getirici özel dönüştürücüye başvurduğunda başvuru işleme durumunun korunmadığını unutmayın. Bu, başvuru koruması etkinleştirilmişken seri hale getirilen veya seri durumundan çıkarılan nesne grafiği içinde yer alan herhangi bir tür için özel bir dönüştürücüye sahipseniz, bu dönüştürücünün ve iç içe geçmiş serileştirme çağrılarının mevcut ReferenceResolver örneğe erişimi olmayacak anlamına gelir.

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

Makalede, 'den 'e geçiş için ek özel dönüştürücü örnekleri bulunmaktadır.

Kaynak kodundaki birim testleri klasörü, aşağıdaki 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