Cara menulis pengonversi kustom untuk serialisasi JSON (penyusunan) di .NET

Artikel ini menunjukkan cara membuat pengonversi kustom untuk kelas serialisasi JSON yang disediakan di namespace layanan System.Text.Json. Untuk pengenalan System.Text.Json, lihat Cara membuat serialisasi dan deserialisasi JSON di .NET.

Pengonversi adalah kelas yang mengonversi objek atau nilai ke dan dari JSON. Namespace layanan System.Text.Json memiliki pengonversi bawaan untuk sebagian besar jenis primitif yang memetakan ke primitif JavaScript. Anda dapat menulis pengonversi kustom untuk mengambil alih perilaku default konverter bawaan. Contohnya:

  • Anda mungkin ingin DateTime nilai diwakili oleh format mm/dd/yyyy. Secara default, ISO 8601-1:2019 didukung, termasuk profil RFC 3339. Untuk informasi selengkapnya, lihat Dukungan DateTime dan DateTimeOffset di System.Text.Json.
  • Anda mungkin ingin menserialisasikan POCO sebagai string JSON, misalnya, dengan PhoneNumber jenis.

Anda juga dapat menulis pengonversi kustom untuk menyesuaikan atau memperluas System.Text.Json dengan fungsionalitas baru. Skenario berikut dibahas nanti dalam artikel ini:

Visual Basic tidak dapat digunakan untuk menulis pengonversi kustom tetapi dapat memanggil pengonversi yang diimplementasikan di pustaka C#. Untuk informasi selengkapnya, lihat dukungan (Visual Basic).

Pola pengonversi kustom

Ada dua pola untuk membuat pengonversi kustom: pola dasar dan pola pabrik. Pola pabrik adalah untuk pengonversi yang menangani jenis Enum atau generik terbuka. Pola dasarnya adalah untuk jenis generik dan non-generik tertutup. Misalnya, non-generik untuk jenis berikut memerlukan pola pabrik:

Beberapa contoh jenis yang dapat ditangani oleh pola dasar meliputi:

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

Pola dasar menciptakan kelas yang dapat menangani satu jenis. Pola pabrik menciptakan kelas yang menentukan, pada waktu berjalan, jenis tertentu mana yang diperlukan dan secara dinamis membuat pengonversi yang sesuai.

Sampel pengonversi dasar

Sampel berikut adalah pengonversi yang menimpa serialisasi default untuk jenis data yang ada. Pengonversi menggunakan format mm/dd/yyyy untuk properti 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));
    }
}

Sampel pengonversi pola pabrik

Kode berikut menunjukkan pengonversi kustom yang berfungsi dengan Dictionary<Enum,TValue>. Kode mengikuti pola pabrik karena parameter jenis generik pertama adalah Enum dan yang kedua terbuka. Metode CanConvert hanya menampilkan true untuk Dictionary dengan dua parameter generik, yang pertama adalah jenis Enum. Pengonversi dalam akan mendapatkan pengonversi yang ada untuk menangani jenis apa pun yang disediakan pada durasi untuk TValue.

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

namespace SystemTextJsonSamples
{
    public class DictionaryTKeyEnumTValueConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType)
            {
                return false;
            }

            if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
            {
                return false;
            }

            return typeToConvert.GetGenericArguments()[0].IsEnum;
        }

        public override JsonConverter CreateConverter(
            Type type,
            JsonSerializerOptions options)
        {
            Type[] typeArguments = type.GetGenericArguments();
            Type keyType = typeArguments[0];
            Type valueType = typeArguments[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    [keyType, valueType]),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: [options],
                culture: null)!;

            return converter;
        }

        private class DictionaryEnumConverterInner<TKey, TValue> :
            JsonConverter<Dictionary<TKey, TValue>> where TKey : struct, Enum
        {
            private readonly JsonConverter<TValue> _valueConverter;
            private readonly Type _keyType;
            private readonly Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter.
                _valueConverter = (JsonConverter<TValue>)options
                    .GetConverter(typeof(TValue));

                // Cache the key and value types.
                _keyType = typeof(TKey);
                _valueType = typeof(TValue);
            }

            public override Dictionary<TKey, TValue> Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
            {
                if (reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }

                var dictionary = new Dictionary<TKey, TValue>();

                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndObject)
                    {
                        return dictionary;
                    }

                    // Get the key.
                    if (reader.TokenType != JsonTokenType.PropertyName)
                    {
                        throw new JsonException();
                    }

                    string? propertyName = reader.GetString();

                    // For performance, parse with ignoreCase:false first.
                    if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) &&
                        !Enum.TryParse(propertyName, ignoreCase: true, out key))
                    {
                        throw new JsonException(
                            $"Unable to convert \"{propertyName}\" to Enum \"{_keyType}\".");
                    }

                    // Get the value.
                    reader.Read();
                    TValue value = _valueConverter.Read(ref reader, _valueType, options)!;

                    // Add to dictionary.
                    dictionary.Add(key, value);
                }

                throw new JsonException();
            }

            public override void Write(
                Utf8JsonWriter writer,
                Dictionary<TKey, TValue> dictionary,
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();

                foreach ((TKey key, TValue value) in dictionary)
                {
                    string propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    _valueConverter.Write(writer, value, options);
                }

                writer.WriteEndObject();
            }
        }
    }
}

Langkah-langkah untuk mengikuti pola dasar

Langkah-langkah berikut menjelaskan cara membuat pengonversi dengan mengikuti pola dasar:

  • Buat kelas yang berasal dari JsonConverter<T> dengan T adalah jenis yang akan diserialisasikan dan dideserialisasi.
  • Ganti metode Read untuk mendeserialisasi JSON yang masuk dan mengonversinya menjadi jenis T. Utf8JsonReader Gunakan yang diteruskan ke metode untuk membaca JSON. Anda tidak perlu khawatir tentang penanganan data parsial, karena pembuat serialisasi meneruskan semua data untuk cakupan JSON saat ini. Jadi tidak perlu memanggil Skip atau TrySkip, atau memvalidasi bahwa Read menampilkan true.
  • Ganti metode Write untuk menserialisasi objek yang masuk jenis T. Gunakan Utf8JsonWriter yang diteruskan ke metode untuk menulis JSON.
  • Ganti metode CanConvert hanya jika perlu. Implementasi default akan menampilkan true jika jenis yang akan dikonversi berjenis T. Oleh karena itu, pengonversi yang hanya mendukung jenis T tidak perlu mengganti metode ini. Untuk contoh pengonversi yang memang perlu mengganti metode ini, lihat bagian deserialisasi polimorfik nanti dalam artikel ini.

Anda dapat merujuk ke kode sumber pengonversi bawaan sebagai implementasi referensi untuk menulis pengonversi kustom.

Langkah-langkah untuk mengikuti pola pabrik

Langkah-langkah berikut menjelaskan cara membuat pengonversi dengan mengikuti pola pabrik:

  • Buat kelas yang berasal dari JsonConverterFactory.
  • Ambil alih metode untuk CanConvert mengembalikan true ketika jenis yang akan dikonversi adalah metode yang dapat ditangani oleh pengonversi. Misalnya, jika pengonversi adalah untuk List<T>, pengonversi mungkin hanya menangani List<int>, List<string>, dan List<DateTime>.
  • Ganti metode CreateConverter untuk menampilkan instans kelas pengonversi yang akan menangani jenis yang akan dikonversi yang disediakan saat durasi.
  • Buat kelas pengonversi yang digunakan metode CreateConverter.

Pola pabrik diperlukan untuk generik terbuka karena kode untuk mengonversi objek ke dan dari string tidak sama untuk semua jenis. Pengonversi untuk jenis generik terbuka (List<T>, misalnya) harus membuat pengonversi untuk jenis generik tertutup (List<DateTime>, misalnya) di belakang layar. Kode harus ditulis untuk menangani setiap jenis generik tertutup yang dapat ditangani pengonversi.

Jenis Enum mirip dengan jenis generik terbuka: pengonversi untuk Enum harus membuat pengonversi untuk Enum tertentu (WeekdaysEnum, misalnya) di belakang layar.

Penggunaan Utf8JsonReader dalam metode Read

Jika pengonversi Anda mengonversi objek JSON, Utf8JsonReader akan diposisikan pada token objek awal saat metode Read dimulai. Anda lalu harus membaca semua token dalam objek tersebut dan keluar dari metode dengan pembaca yang diposisikan pada token objek akhir yang sesuai. Jika Anda membaca di luar akhir objek, atau jika Anda berhenti sebelum mencapai token akhir yang sesuai, Anda mendapatkan pengecualian JsonException yang menunjukkan bahwa:

Pengonversi 'ConverterName' membaca terlalu banyak atau tidak cukup.

Misalnya, lihat pengonversi sampel pola pabrik sebelumnya. Metode Read dimulai dengan memastikan bahwa pembaca diposisikan pada token objek awal. Ini membaca sampai menemukan bahwa ini diposisikan pada token objek akhir berikutnya. Ini berhenti pada token objek akhir berikutnya karena tidak ada token objek mulai intervensi yang akan menunjukkan objek dalam objek. Aturan yang sama tentang token awal dan token akhir berlaku jika Anda mengonversi array. Misalnya, lihat sampel pengonversi Stack<T> nanti dalam artikel ini.

Penanganan kesalahan

Pembuat serialisasi menyediakan penanganan khusus untuk jenis pengecualian JsonException dan NotSupportedException.

JsonException

Jika Anda menampilkan JsonException tanpa pesan, pembuat serialisasi membuat pesan yang menyertakan jalur ke bagian JSON yang menyebabkan kesalahan. Misalnya, pernyataan throw new JsonException() menghasilkan pesan kesalahan seperti contoh berikut:

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

Jika Anda memberikan pesan (misalnya, throw new JsonException("Error occurred")), pembuat serialisasi tetap mengatur properti Path, LineNumber, dan BytePositionInLine.

NotSupportedException

Jika Anda menampilkan NotSupportedException, Anda akan mendapatkan informasi jalur dalam pesan kapan saja. Jika Anda memberikan pesan, informasi jalur akan ditambahkan ke dalamnya. Misalnya, pernyataan throw new NotSupportedException("Error occurred.") menghasilkan pesan kesalahan seperti contoh berikut:

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

Kapan harus menampilkan jenis pengecualian

Jika payload JSON berisi token yang tidak valid untuk jenis yang dideserialisasi, tampilkan JsonException.

Jika Anda ingin melarang jenis tertentu, tampilkan NotSupportedException. Pengecualian inilah yang secara otomatis dilemparkan oleh pembuat serialisasi untuk jenis yang tidak didukung. Misalnya, System.Type tidak didukung karena alasan keamanan, jadi upaya untuk mendeserialisasinya akan menghasilkan NotSupportedException.

Anda dapat menampilkan pengecualian lain sesuai kebutuhan, tetapi tidak secara otomatis menyertakan informasi jalur JSON.

Mendaftarkan pengonversi kustom

Daftarkan pengonversi kustom agar metode Serialize dan Deserialize menggunakannya. Pilih salah satu pendekatan berikut:

Sampel pendaftaran - Kumpulan pengonversi

Berikut ini contoh yang menjadikan DateTimeOffsetJsonConverter sebagai default untuk properti jenis DateTimeOffset:

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

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Misalkan Anda melakukan serialisasi instans dari jenis berikut:

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

Berikut ini contoh output JSON yang menunjukkan pengonversi kustom digunakan:

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

Kode berikut menggunakan pendekatan yang sama untuk mendeserialisasi menggunakan pengonversi kustom DateTimeOffset:

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

Sampel pendaftaran - [JsonConverter] di properti

Kode berikut memilih pengonversi kustom untuk properti Date:

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

Kode untuk serialisasi WeatherForecastWithConverterAttribute tidak memerlukan penggunaan JsonSerializeOptions.Converters:

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

Kode untuk deserialisasi juga tidak memerlukan penggunaan Converters:

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

Sampel pendaftaran - [JsonConverter] pada jenis

Berikut ini kode yang membuat struktur dan menerapkan atribut [JsonConverter] ke struktur tersebut:

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

Berikut adalah pengonversi kustom untuk struktur sebelumnya:

using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class TemperatureConverter : JsonConverter<Temperature>
    {
        public override Temperature Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                Temperature.Parse(reader.GetString()!);

        public override void Write(
            Utf8JsonWriter writer,
            Temperature temperature,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(temperature.ToString());
    }
}

Atribut [JsonConverter] pada struktur mendaftarkan pengonversi kustom sebagai default untuk properti jenis Temperature. Pengonversi secara otomatis digunakan pada properti TemperatureCelsius jenis berikut saat Anda menserialisasikan atau mendeserialisasikannya:

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

Prioritas pendaftaran pengonversi

Selama serialisasi atau deserialisasi, pengonversi dipilih untuk setiap elemen JSON dalam urutan berikut, tercantum dari prioritas tertinggi ke terendah:

  • [JsonConverter] diterapkan ke properti.
  • Pengonversi ditambahkan ke kumpulan Converters.
  • [JsonConverter] diterapkan ke jenis nilai kustom atau POCO.

Jika beberapa pengonversi kustom untuk jenis terdaftar dalam Converters koleksi, pengonversi pertama yang mengembalikan true untuk CanConvert digunakan.

Pengonversi bawaan hanya akan dipilih jika tidak ada pengonversi kustom yang berlaku yang terdaftar.

Sampel pengonversi untuk skenario umum

Bagian berikut menyediakan sampel pengonversi yang membahas beberapa skenario umum yang tidak ditangani oleh fungsionalitas bawaan.

Untuk pengonversi sampel DataTable, lihat Jenis koleksi yang didukung.

Deserialisasi jenis yang disimpulkan ke properti objek

Saat deserialisasi ke properti jenis object, objek JsonElement akan dibuat. Alasannya adalah bahwa pembuat deserialisasi tidak tahu jenis CLR apa yang harus dibuat, dan tidak mencoba menebak. Misalnya, jika properti JSON memiliki "true", pembuat deserialisasi tidak menyimpulkan bahwa nilainya adalah Boolean, dan jika elemen memiliki "01/01/2019", pembuat deserialisasi tidak menyimpulkan bahwa itu adalah DateTime.

Inferensi jenis bisa tidak akurat. Jika pembuat deserialisasi mengurai angka JSON yang tidak memiliki titik desimal sebagai long, yang dapat mengakibatkan masalah di luar rentang jika nilai awalnya diserialisasikan sebagai ulong atau BigInteger. Mengurai angka yang memiliki titik desimal sebagai double mungkin kehilangan presisi jika angka awalnya diserialisasikan sebagai decimal.

Untuk skenario yang memerlukan inferensi jenis, kode berikut menunjukkan pengonversi kustom untuk properti object. Kode akan mengonversi:

  • true dan false ke Boolean
  • Angka tanpa desimal ke long
  • Angka dengan desimal ke double
  • Tanggal ke DateTime
  • String ke string
  • Segala sesuatu yang lain ke 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"
//}

Contoh menunjukkan kode pengonversi dan kelas WeatherForecast dengan properti object. Metode Main ini mendeserialisasi string JSON menjadi instans WeatherForecast, tanpa menggunakan pengonversi terlebih dahulu, lalu menggunakan pengonversi. Output konsol menunjukkan bahwa tanpa pengonversi, jenis run-time untuk Date properti adalah JsonElement; dengan pengonversi, jenis run-time adalah DateTime.

Folder pengujian unit di namespace layanan System.Text.Json.Serialization memiliki lebih banyak contoh pengonversi kustom yang menangani deserialisasi ke properti object.

Mendukung deserialisasi polimorfik

.NET 7 menyediakan dukungan untuk serialisasi polimorfik dan deserialisasi. Namun, dalam versi .NET sebelumnya, ada dukungan serialisasi polimorfik terbatas dan tidak ada dukungan untuk deserialisasi. Jika Anda menggunakan .NET 6 atau versi yang lebih lama, deserialisasi memerlukan pengonversi kustom.

Misalkan, sebagai contoh, Anda memiliki kelas dasar abstrak Person, dengan kelas turunan Employee dan Customer. Deserialisasi polimorfik berarti bahwa pada waktu desain, Anda dapat menentukan Person sebagai target deserialisasi, dan objek Customer dan Employee di JSON dideserialisasi dengan benar pada durasi. Selama deserialisasi, Anda harus menemukan petunjuk yang mengidentifikasi jenis yang diperlukan di JSON. Jenis petunjuk yang tersedia bervariasi menurut setiap skenario. Misalnya, properti pembeda mungkin tersedia atau Anda mungkin harus bergantung pada ada atau tidak adanya properti tertentu. Rilis System.Text.Json saat ini tidak menyediakan atribut untuk menentukan cara menangani skenario deserialisasi polimorfik, sehingga diperlukan pengonversi kustom.

Kode berikut menunjukkan kelas dasar, dua kelas turunan, dan pengonversi kustom untuk mereka. Pengonversi menggunakan properti pembeda untuk melakukan deserialisasi polimorfik. Jenis pembeda tidak ada dalam definisi kelas tetapi dibuat selama serialisasi dan dibaca selama deserialisasi.

Penting

Kode contoh mengharuskan pasangan nama/nilai objek JSON untuk tetap berurutan, yang bukan persyaratan standar JSON.

public class Person
{
    public string? Name { get; set; }
}

public class Customer : Person
{
    public decimal CreditLimit { get; set; }
}

public class Employee : Person
{
    public string? OfficeNumber { get; set; }
}
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class PersonConverterWithTypeDiscriminator : JsonConverter<Person>
    {
        enum TypeDiscriminator
        {
            Customer = 1,
            Employee = 2
        }

        public override bool CanConvert(Type typeToConvert) =>
            typeof(Person).IsAssignableFrom(typeToConvert);

        public override Person Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException();
            }

            string? propertyName = reader.GetString();
            if (propertyName != "TypeDiscriminator")
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.Number)
            {
                throw new JsonException();
            }

            TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
            Person person = typeDiscriminator switch
            {
                TypeDiscriminator.Customer => new Customer(),
                TypeDiscriminator.Employee => new Employee(),
                _ => throw new JsonException()
            };

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    return person;
                }

                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    propertyName = reader.GetString();
                    reader.Read();
                    switch (propertyName)
                    {
                        case "CreditLimit":
                            decimal creditLimit = reader.GetDecimal();
                            ((Customer)person).CreditLimit = creditLimit;
                            break;
                        case "OfficeNumber":
                            string? officeNumber = reader.GetString();
                            ((Employee)person).OfficeNumber = officeNumber;
                            break;
                        case "Name":
                            string? name = reader.GetString();
                            person.Name = name;
                            break;
                    }
                }
            }

            throw new JsonException();
        }

        public override void Write(
            Utf8JsonWriter writer, Person person, JsonSerializerOptions options)
        {
            writer.WriteStartObject();

            if (person is Customer customer)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Customer);
                writer.WriteNumber("CreditLimit", customer.CreditLimit);
            }
            else if (person is Employee employee)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Employee);
                writer.WriteString("OfficeNumber", employee.OfficeNumber);
            }

            writer.WriteString("Name", person.Name);

            writer.WriteEndObject();
        }
    }
}

Kode berikut mendaftarkan pengonversi:

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

Pengonversi dapat mendeserialisasi JSON yang dibuat dengan menggunakan pengonversi yang sama untuk serialisasi, misalnya:

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

Kode pengonversi dalam contoh sebelumnya membaca dan menulis setiap properti secara manual. Alternatifnya adalah memanggil Deserialize atau Serialize untuk melakukan beberapa pekerjaan. Misalnya, lihat posting StackOverflow ini.

Cara alternatif untuk melakukan deserialisasi polimorfik

Anda dapat memanggil Deserialize dalam metode Read:

  • Buat klon instans Utf8JsonReader. Karena Utf8JsonReader adalah struktur, ini hanya memerlukan pernyataan penetapan.
  • Gunakan klon untuk membaca token pembeda.
  • Panggil Deserialize menggunakan instans Reader asli setelah Anda mengetahui jenis yang Anda butuhkan. Anda dapat memanggil Deserialize karena instans Reader asli tetap diposisikan untuk membaca token objek awal.

Kerugian dari metode ini adalah Anda tidak dapat meneruskan instans opsi asli yang mendaftarkan pengonversi ke Deserialize. Melakukannya akan menyebabkan luapan tumpukan, seperti yang dijelaskan dalam Properti yang diperlukan. Contoh berikut menunjukkan metode Read yang menggunakan alternatif ini:

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

Mendukung perjalanan pulang pergi untuk Stack jenis

Jika Anda mendeserialisasi string JSON menjadi objek Stack dan kemudian membuat serialisasi objek tersebut, konten tumpukan dalam urutan terbalik. Perilaku ini berlaku untuk jenis dan antarmuka berikut, dan jenis yang ditentukan pengguna yang berasal darinya:

Untuk mendukung serialisasi dan deserialisasi yang mempertahankan urutan asli dalam tumpukan, diperlukan pengonversi kustom.

Kode berikut menunjukkan pengonversi kustom yang memungkinkan komunikasi dua arah ke dan dari Stack<T> objek:

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

Kode berikut mendaftarkan pengonversi:

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

Kebijakan penamaan untuk deserialisasi string enum

Secara default, JsonStringEnumConverter bawaan dapat menserialisasi dan mendeserialisasi nilai string untuk enum. Ini berfungsi tanpa kebijakan penamaan tertentu atau dengan kebijakan penamaan CamelCase. Itu tidak mendukung kebijakan penamaan lainnya, seperti snake case. Untuk informasi tentang kode pengonversi kustom yang dapat mendukung komunikasi dua arah ke dan dari nilai string enum saat menggunakan kebijakan penamaan snake case, lihat masalah GitHub dotnet/runtime #31619. Atau, tingkatkan ke .NET 7 atau versi yang lebih baru, yang menyediakan dukungan bawaan untuk menerapkan kebijakan penamaan saat melakukan round-tripping ke dan dari nilai string enum.

Menggunakan pengonversi sistem default

Dalam beberapa skenario, Anda mungkin ingin menggunakan pengonversi sistem default dalam pengonversi kustom. Untuk melakukannya, dapatkan pengonversi sistem dari properti JsonSerializerOptions.Default, seperti yang ditunjukkan dalam contoh berikut:

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

Menangani nilai null

Secara default, pembuat serialisasi menangani nilai null sebagai berikut:

  • Untuk jenis referensi dan jenis Nullable<T>:

    • Ini tidak meneruskan null ke pengonversi kustom pada serialisasi.
    • Ini tidak meneruskan JsonTokenType.Null ke pengonversi kustom saat deserialisasi.
    • Ini akan menampilkan instans null saat deserialisasi.
    • Ini akan menulis null langsung dengan penulis saat serialisasi.
  • Untuk jenis nilai yang tidak dapat diubah ke null:

    • Ini meneruskan JsonTokenType.Null ke pengonversi kustom saat deserialisasi. (Jika tidak ada pengonversi kustom yang tersedia, pengecualian JsonException akan ditampilkan oleh pengonversi internal untuk jenis tersebut.)

Perilaku penanganan null ini terutama untuk mengoptimalkan performa dengan melewatkan panggilan tambahan ke pengonversi. Selain itu, ini menghindari memaksa pengonversi untuk jenis yang dapat diubah ke null untuk diperiksa untuk null di awal setiap penggantian metode Read dan Write.

Untuk mengaktifkan pengonversi kustom untuk menangani null untuk jenis nilai atau referensi, ganti JsonConverter<T>.HandleNull untuk menampilkan true, seperti yang ditunjukkan dalam contoh berikut:

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.

Mempertahankan referensi

Secara default, data referensi hanya di-cache untuk setiap panggilan ke Serialize atau Deserialize. Untuk mempertahankan referensi dari satu panggilan Serialize/Deserialize ke panggilan lain, akar instans ReferenceResolver di situs panggilan Serialize/Deserialize. Kode berikut menunjukkan contoh untuk skenario ini:

  • Anda menulis pengonversi kustom untuk jenis Company.
  • Anda tidak ingin menserialisasi properti Supervisor secara manual, yang merupakan Employee. Anda ingin mendelegasikannya ke pembuat serialisasi dan Anda juga ingin menyimpan referensi yang telah Anda simpan.

Berikut ini kelas Employee dan Company:

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

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

Pengonversi akan terlihat seperti ini:

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

Kelas yang berasal dari ReferenceResolver menyimpan referensi dalam kamus:

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

Kelas yang berasal dari ReferenceHandler menyimpan instans MyReferenceResolver dan membuat instans baru hanya jika diperlukan (dalam metode bernama Reset dalam contoh ini):

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

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

Saat kode sampel memanggil pembuat serialisasi, kode tersebut menggunakan instans JsonSerializerOptions di mana properti ReferenceHandler diatur ke instans MyReferenceHandler. Jika Anda mengikuti pola ini, pastikan untuk mengatur ulang kamus ReferenceResolver saat Anda selesai melakukan serialisasi, agar tidak terus berkembang.

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

Contoh sebelumnya hanya melakukan serialisasi, tetapi pendekatan serupa dapat diadopsi untuk deserialisasi.

Sampel pengonversi kustom lainnya

Artikel Migrasi dari Newtonsoft.Json ke System.Text.Json berisi sampel tambahan pengonversi kustom.

Folder pengujian unit dalam kode sumber System.Text.Json.Serialization menyertakan sampel pengonversi kustom lain, seperti:

Jika Anda perlu membuat pengonversi yang mengubah perilaku pengonversi bawaan yang ada, Anda bisa mendapatkan kode sumber pengonversi yang ada untuk berfungsi sebagai titik awal penyesuaian.

Sumber Daya Tambahan: