Mert Özen Her Satırda Daha İleri
Backend .NET'te I/O İşlemleri — #04

System.Text.Json Derinlemesine 1: Options, Converter'lar, Polymorphism ve Strict Mode

Mert Özen 14 Kas 2026 13 dk 1 görüntülenme
System.Text.Json Derinlemesine 1: Options, Converter'lar, Polymorphism ve Strict Mode

System.Text.Json varsayılan olarak duplicate property'de son değeri alır, eksik ve bilinmeyen alanları sessizce geçer; dışarıdan gelen payload'larda .NET 10'un JsonSerializerOptions.Strict ve AllowDuplicateProperties = false ayarlarıyla bunu kapatmak gerekir. Web varsayılanlarını, naming policy'leri, required ve nullable kurallarını, Bookstore'un PaymentMethod hiyerarşisiyle polymorphism'i, Money converter'ını ve contract customization'ı gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (System.Text.Json 10) ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) ödeme service'ine şu istek geliyor:

{"OrderNumber":"BS-0000042","Amount":"10.00 TRY","Amount":"9999.00 TRY","Method":{"type":"card","Brand":"Visa","Last4":"4242"}}

Amount iki kez yazılmış. JsonSerializer.Deserialize<PaymentRequest> exception mı fırlatır, ilk değeri mi alır, sonuncuyu mu? Önünde tutarı kontrol eden başka bir service varsa ve o ilk değeri okuyorsa ne olur?

İkinci soru: Angular istemcisi {"orderNumber":"BS-0000042","customerName":"Ada"} gönderiyor, C# tarafında property'ler OrderNumber ve CustomerName. Bir console uygulamasında JsonSerializer.Deserialize<Order>(json) çağırırsan OrderNumber ne olur?

Üçüncüsü: bir ödeme yönteminin JSON'unda "type":"card" alanı en sonda geliyor. Polymorphic deserialization bunu bulur mu?

Bu yazı STJ'nin (System.Text.Json kütüphanesinin kısa adı) ayarlarını ve model kurallarını, .NET 10'un Strict ayarlarını ve polymorphism'i ele alıyor. Örnekler serinin repo'sunda Bookstore.IO.Json console uygulamasında:

dotnet run --project src/Bookstore.IO.Json -- duplicates

Varsayılanlar ve JsonSerializerOptions.Web

STJ'nin hiçbir ayar verilmeden kullanılan varsayılanları ile ASP.NET Core'un kullandığı web varsayılanları farklıdır. Aynı siparişi iki şekilde serialize edelim:

Console.WriteLine($"Default: {JsonSerializer.Serialize(order)}");
Console.WriteLine($"Web:     {JsonSerializer.Serialize(order, JsonSerializerOptions.Web)}");
Default: {"OrderNumber":"BS-0000042","CustomerName":"Ada","CreatedAtUtc":"2026-11-14T09:30:00Z","Status":1,"Currency":"TRY","Items":[{"Isbn":"9780441172719","Title":"Dune","Quantity":2,"UnitPrice":349.90,"LineTotal":699.80}],"Total":699.80}
Web:     {"orderNumber":"BS-0000042","customerName":"Ada","createdAtUtc":"2026-11-14T09:30:00Z","status":1,"currency":"TRY","items":[{"isbn":"9780441172719","title":"Dune","quantity":2,"unitPrice":349.90,"lineTotal":699.80}],"total":699.80}
Web options: case-insensitive=True, numbers=AllowReadingFromString, naming=JsonCamelCaseNamingPolicy

JsonSerializerOptions.Web .NET 9 ile geldi ve ASP.NET Core'un kullandığı ayarların salt okunur bir kopyası: property adları camelCase yazılır, okurken büyük/küçük harf farkı önemsenmez ve tırnak içinde gelen sayılar kabul edilir. Varsayılan ayarlar ise property adını olduğu gibi yazar ve okurken harf duyarlıdır.

İkinci sorunun cevabı da burada. camelCase JSON'u varsayılan ayarlarla okuyunca hiçbir property eşleşmez:

Default: OrderNumber=null, Items=null
Web:     OrderNumber=BS-0000042, Items=0

Exception yok, sessizce boş bir nesne geldi. Dikkat edersen Items nullable olmayan bir IReadOnlyList<OrderItem> olarak tanımlı ama null geldi; varsayılan ayarlar nullable annotation'lara bakmıyor. Bu hatanın en sık görüldüğü yer, ASP.NET Core'dan dönen JSON'u bir console uygulamasında, bir test projesinde ya da bir Worker'da new JsonSerializerOptions() ile okumak. Web API ile konuşan her kodda JsonSerializerOptions.Web'i ya da ondan türetilmiş bir instance'ı kullan.

Son çıktıdaki Total ve LineTotal'a da dikkat: getter'ı olan her public property serialize edilir, hesaplanan değerler dahil. Okurken bu alanlar sessizce atlanır, çünkü set edilecek bir yerleri yok.

Naming Policy, Enum'lar ve Sayılar

Bir tedarikçi ya da ödeme sağlayıcı snake_case bekliyorsa .NET 8 ile gelen hazır policy'leri kullanabilirsin: SnakeCaseLower, SnakeCaseUpper, KebabCaseLower, KebabCaseUpper. Enum'lar varsayılan olarak sayı yazılır; JsonStringEnumConverter onları ada çevirir ve aynı policy'yi enum değerlerine de uygulayabilir:

var snake = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
    Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }
};
Snake:   {"order_number":"BS-0000042","customer_name":"Ada","created_at_utc":"2026-11-14T09:30:00Z","status":"paid","currency":"TRY","items":[{"isbn":"9780441172719","title":"Dune","quantity":2,"unit_price":349.90,"line_total":699.80}],"total":699.80}

Enum değerini sayı olarak taşımak kırılgandır: enum'un ortasına yeni bir değer eklenirse sonraki değerlerin sayısı kayar ve eski istemciler yanlış durumu okur. API contract'larında enum'ları string olarak taşımak daha güvenli. Tek bir değerin adını özel olarak belirlemen gerekirse .NET 9 ile gelen [JsonStringEnumMemberName] attribute'u var; aşağıdaki polymorphism örneğinde RefundPending'in "refund-pending" olarak yazıldığını göreceksin.

Sayılarda iki ayrıntı var. Varsayılan ayarlar tırnak içindeki sayıyı reddeder; Web ayarları JsonNumberHandling.AllowReadingFromString ile kabul eder:

Default: JsonException: The JSON value could not be converted to Bookstore.Ordering.OrderItem. Path: $.quantity | LineNumber: 0 | BytePositionInLine: 53.
Web:     Dune x2 @ 349.90

İkincisi decimal. STJ decimal'ı JSON number olarak yazar ve 349.90'daki sondaki sıfırı korur. Karşı taraf JavaScript ise sayıyı double olarak okuyacağı için büyük tutarlarda ve hassas kuruş hesaplarında para değerini string olarak taşımak daha güvenli. Biz de birazdan Money için bunu yapacağız.

Model Kuralları: required, Constructor ve Nullable

Deserialization'ın en tehlikeli varsayılanı, eksik alanları sessizce default değerle bırakması. STJ'de bunu sıkılaştırmanın birkaç yolu var ve her biri farklı bir boşluğu kapatıyor:

public sealed class PaymentRequest
{
    public required string OrderNumber { get; init; }
    public required Money Amount { get; init; }
    public required PaymentMethod Method { get; init; }
    public PaymentStatus Status { get; init; }
    public DateOnly? DueDate { get; init; }
    public string? Note { get; init; }
}
required: JsonException: JSON deserialization for type 'Bookstore.IO.Json.Payments.PaymentRequest' was missing required properties including: 'Amount'.
unmapped: JsonException: The JSON property 'Discount' could not be mapped to any .NET member contained in type 'Bookstore.Ordering.OrderItem'.
default: Title is null
nullable: JsonException: The constructor parameter 'Title' on type 'Bookstore.Ordering.OrderItem' doesn't allow null values. Consider updating its nullability annotation. Path: $.Title | LineNumber: 0 | BytePositionInLine: 36.
default: missing Title becomes null
ctor: JsonException: JSON deserialization for type 'Bookstore.Ordering.OrderItem' was missing required properties including: 'Title'.

Sırayla: C#'ın required modifier'ı STJ tarafından da uygulanır; JSON'da o property yoksa exception gelir. UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow (.NET 8), modelde karşılığı olmayan bir alan gelince hata verir. İstemci Discount göndermiş ama modelde böyle bir alan yok; varsayılan davranış bunu yok saymak, yani istemcinin "indirim uyguladım" sandığı bir isteği sessizce indirimsiz işlemek.

Son iki satır record'lar için önemli. OrderItem primary constructor'lı bir record ve Title nullable olmayan bir string. Varsayılan ayarlarla hem "Title":null hem de hiç gönderilmemiş bir Title null olarak geliyor. .NET 9 ile gelen RespectNullableAnnotations ilk durumu, RespectRequiredConstructorParameters ikinci durumu yakalıyor. İkisi de varsayılan olarak kapalı, çünkü açmak mevcut uygulamaları kırabilir.

Bir de ters yönlü bir ayar var: JsonObjectCreationHandling.Populate (.NET 8). Varsayılan olarak STJ, JSON'da gelen bir collection property için yeni bir instance yaratıp eskisinin yerine koyar. Populate ile mevcut instance'ın üzerine ekler:

public sealed class BookFilter
{
    public List<string> Tags { get; set; } = ["in-stock"];
}
Replace:  [new]
Populate: [in-stock, new]

Varsayılan değerleri olan configuration nesnelerinde ya da salt okunur collection property'lerinde işe yarar. Ama hangisinin istendiği bağlama bağlı; bir API isteğinde Tags'i gönderen istemci büyük ihtimalle listenin tamamını göndermek istiyordur.

.NET 10: Strict ve Duplicate Property

İlk sorunun cevabı:

var payment = JsonSerializer.Deserialize<PaymentRequest>(json)!;
Console.WriteLine($"Default: Amount = {payment.Amount}");

using (var doc = JsonDocument.Parse(json))
{
    JsonElement first = doc.RootElement.EnumerateObject().First(p => p.Name == "Amount").Value;
    Console.WriteLine($"JsonDocument, first Amount property: {first}");
}
Default: Amount = 9999.00 TRY
JsonDocument, first Amount property: 10.00 TRY
AllowDuplicateProperties=false: JsonException: Duplicate property 'Amount' encountered during deserialization of type 'Bookstore.IO.Json.Payments.PaymentRequest'.
Strict: JsonException: Duplicate property 'Amount' encountered during deserialization of type 'Bookstore.IO.Json.Payments.PaymentRequest'.

Varsayılan ayarlarla son değer kazanıyor: tutar 9.999 TRY (Türk lirasının uluslararası para birimi kodu). JSON spesifikasyonu duplicate property'nin nasıl ele alınacağını tanımlamaz ve farklı parser'lar farklı davranır; bazıları ilk değeri, bazıları sonuncuyu alır, bazıları hata verir. Güvenlik riski tam burada: önde tutarı ve limiti kontrol eden bir gateway ya da fraud kontrolü ilk değeri (10 TRY) okuyup isteği onaylar, arkadaki ödeme service'i sonuncuyu (9.999 TRY) işler. İki bileşen aynı isteği farklı okur ve kontrol atlatılır. Örnekte bunu aynı process içinde bile gördük: JsonDocument üzerinde ilk eşleşmeyi alan kod 10 TRY buldu.

.NET 10 bunun için iki şey getirdi. AllowDuplicateProperties = false, aynı property ikinci kez geldiğinde exception fırlatır; JsonDocumentOptions'ta da aynı ayar var. JsonSerializerOptions.Strict ise birkaç güvenli ayarı tek bir hazır instance'ta toplar:

Strict: unmapped=Disallow, duplicates=False, case-insensitive=False, nullable=True, ctor=True
Strict reads default output: Dune
camelCase: JsonException: The JSON property 'isbn' could not be mapped to any .NET member contained in type 'Bookstore.Ordering.OrderItem'.
Strict + camelCase: Dune x2

Yani Strict: bilinmeyen alan yok, duplicate yok, harf duyarlı eşleşme, nullable annotation'lara ve zorunlu constructor parametrelerine uyma. Microsoft'un notuna göre Strict, varsayılan ayarlarla serialize edilmiş JSON'u okuyabilir. Ama web ayarlarıyla (camelCase) üretilmiş JSON'u okuyamaz; örnekteki hata bu. Bir Web API'de kullanmak istiyorsan Strict'ten türetip naming policy'yi eklemen gerekir:

var strictWeb = new JsonSerializerOptions(JsonSerializerOptions.Strict)
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

Dışarıdan gelen ve parayla, yetkiyle ya da stokla ilgili olan her payload'da bu sıkı ayarlar mantıklı bir başlangıç. Kendi service'lerinin arasında, sürüm farkı yüzünden yeni alanların gelebildiği iç trafikte ise UnmappedMemberHandling.Disallow rolling deploy sırasında sorun çıkarabilir; orada duplicate kontrolünü açık tutup bilinmeyen alanlara izin vermek daha pratik olabilir.

Polymorphism: PaymentMethod Hiyerarşisi

Bookstore'da ödeme kartla ya da havale ile yapılabiliyor. Hiyerarşiyi STJ'ye attribute'larla tanıtıyoruz:

[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(CardPayment), "card")]
[JsonDerivedType(typeof(BankTransferPayment), "bank-transfer")]
public abstract record PaymentMethod;

public sealed record CardPayment(string Brand, string Last4) : PaymentMethod;

public sealed record BankTransferPayment(string Iban, string Reference) : PaymentMethod;
{"orderNumber":"BS-0000042","amount":"699.80 TRY","method":{"type":"bank-transfer","iban":"TR000000000000000000000000","reference":"BS-0000042"},"status":"refund-pending","dueDate":"2026-11-21","note":null}
Round trip: BankTransferPayment, 699.80 TRY, RefundPending
[{"type":"card","brand":"Visa","last4":"4242"},{"type":"bank-transfer","iban":"TR000000000000000000000000","reference":"BS-0000043"}]

Discriminator olarak C# tip adını değil, sabit ve kısa bir string kullanıyoruz ("card", "bank-transfer"). Böylece class'ın adını ya da namespace'ini değiştirmek contract'ı bozmaz ve istemci yalnız izin verilen tiplerden birini seçebilir. Şimdi üçüncü soruya ve hata durumlarına bakalım:

discriminator last: NotSupportedException: The JSON payload for polymorphic interface or abstract type 'Bookstore.IO.Json.Payments.PaymentMethod' must specify a type discriminator. Path: $ | LineNumber: 0 | BytePositionInLine: 9.
AllowOutOfOrderMetadataProperties: CardPayment { Brand = Visa, Last4 = 4242 }
unknown type: JsonException: Read unrecognized type discriminator id 'crypto'. Path: $ | LineNumber: 0 | BytePositionInLine: 26.
no type: NotSupportedException: The JSON payload for polymorphic interface or abstract type 'Bookstore.IO.Json.Payments.PaymentMethod' must specify a type discriminator. Path: $ | LineNumber: 0 | BytePositionInLine: 9.

STJ varsayılan olarak discriminator'ı nesnenin ilk property'si olarak bekler, çünkü nesneyi baştan sona tek geçişte okur ve hangi tipi yaratacağını en başta bilmek ister. "type" sonda geldiğinde hata mesajı, hiç gönderilmemiş gibi aynı. STJ'nin kendi ürettiği JSON'da discriminator hep başta olduğu için sorun çıkmaz; ama JSON'u başka bir dil ya da elle yazılmış bir istemci üretiyorsa alanların sırası garanti değildir. .NET 9 ile gelen AllowOutOfOrderMetadataProperties = true bunu çözer; bedeli, STJ'nin nesneyi tipini bulana kadar buffer'lamasıdır.

İki ayrıntı daha. Bilinmeyen bir discriminator JsonException, eksik discriminator ise abstract bir base type'ta NotSupportedException üretir; hata yakalayan kodun ikisini de karşılaması gerekir. Base type abstract olmasaydı, discriminator'ı olmayan bir payload sessizce base type olarak okunurdu. Hiyerarşilerde base type'ı abstract tutmak bu yüzden daha güvenli.

Custom Converter: Money ve DateOnly

Bookstore'da para bir tutar ve bir para biriminden oluşuyor ve contract'ta tek bir string olarak taşınıyor: "249.90 TRY". Bunu bir JsonConverter<T> ile yapıyoruz:

[JsonConverter(typeof(MoneyJsonConverter))]
public readonly record struct Money(decimal Amount, string Currency)
{
    public override string ToString() => $"{Amount.ToString("0.00", CultureInfo.InvariantCulture)} {Currency}";
}

public sealed class MoneyJsonConverter : JsonConverter<Money>
{
    public override Money Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.String)
        {
            throw new JsonException($"Money must be a string like \"249.90 TRY\", got {reader.TokenType}.");
        }

        string text = reader.GetString()!;
        int space = text.IndexOf(' ');
        if (space <= 0
            || !decimal.TryParse(text.AsSpan(0, space), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out decimal amount)
            || text.Length - space - 1 != 3)
        {
            throw new JsonException($"Invalid money value \"{text}\".");
        }
        return new Money(amount, text[(space + 1)..]);
    }

    public override void Write(Utf8JsonWriter writer, Money value, JsonSerializerOptions options) =>
        writer.WriteStringValue(value.ToString());
}
money: JsonException: Money must be a string like "249.90 TRY", got Number.
money: JsonException: Invalid money value "249,90 TL".
Money: 249.90 TRY

Converter'da üç kural önemli. Biçimlendirme ve parse her zaman CultureInfo.InvariantCulture ile yapılır; yoksa Türkçe Windows'ta 249,90 yazan bir sunucu ile 249.90 bekleyen bir istemci arasında para kayar. Geçersiz girdide JsonException fırlatılır; STJ bu exception'a path ve satır bilgisini ekler ve ASP.NET Core onu 400 Bad Request'e çevirir. Converter'ı attribute ile tipe bağlamak, o tip her kullanıldığında (property, collection element, polymorphic alt tip) converter'ın devreye girmesini sağlar.

DateOnly .NET 7'den beri ISO (Uluslararası Standardizasyon Örgütü) 8601 standardındaki biçimde ("2026-11-14") doğrudan destekleniyor. Bir iş ortağı tarihleri "14.11.2026" diye gönderiyorsa, bu formatı yalnız o entegrasyonun options'ına eklenen bir converter ile çözmek, bütün API'nin formatını değiştirmekten iyidir:

public sealed class DayMonthYearDateOnlyConverter : JsonConverter<DateOnly>
{
    private const string Format = "dd.MM.yyyy";

    public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
        DateOnly.ParseExact(reader.GetString()!, Format, CultureInfo.InvariantCulture);

    public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) =>
        writer.WriteStringValue(value.ToString(Format, CultureInfo.InvariantCulture));
}
DateOnly default: "2026-11-14"
DateOnly partner: "14.11.2026"
Parsed: 2026-11-21

Contract Customization: TypeInfoResolver Modifier'ları

Bazen bir tipin JSON contract'ını değiştirmek istersin ama tipe dokunamazsın ya da attribute koymak istemezsin. .NET 7 ile gelen contract customization, STJ'nin bir tip için çıkardığı metadata'yı (JsonTypeInfo) serialization başlamadan önce düzenlemeni sağlar. Örnek: aynı Customer nesnesi API'de telefon numarasıyla, log'larda telefonsuz yazılsın.

public sealed record Customer(string Name, string Email)
{
    [SensitiveData]
    public string? Phone { get; init; }
}

var forLogs = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
        Modifiers = { HideSensitiveData }
    }
};

// Contract customization: [SensitiveData] ile işaretli her property'yi çıkar.
private static void HideSensitiveData(JsonTypeInfo typeInfo)
{
    if (typeInfo.Kind != JsonTypeInfoKind.Object) return;
    foreach (JsonPropertyInfo property in typeInfo.Properties)
    {
        if (property.AttributeProvider?.IsDefined(typeof(SensitiveDataAttribute), inherit: true) == true)
        {
            property.ShouldSerialize = static (_, _) => false;
        }
    }
}
API:  {"Name":"Ada","Email":"ada@example.com","Phone":"+90 555 000 00 00"}
Logs: {"Name":"Ada","Email":"ada@example.com"}

Modifier her tip için bir kez çalışır ve sonuç options instance'ında cache'lenir; her serialization'da reflection maliyeti yoktur. Aynı yöntemle bir property'yi yeniden adlandırabilir, salt okunur hâle getirebilir ya da bir tipe polymorphism bilgisini attribute kullanmadan ekleyebilirsin. Çıktıdaki + ise ayrı bir konu: STJ'nin varsayılan encoder'ı +, <, > gibi karakterleri, JSON bir HTML sayfasına gömülse bile güvenli kalsın diye kaçışlar. JSON olarak geçerli ve her parser doğru okur; yalnız gözle okurken şaşırtabilir.

Üretimde Dikkat

JsonSerializerOptions instance'ını cache'le. Her options instance'ı, gördüğü tipler için metadata üretir ve saklar; her çağrıda new JsonSerializerOptions { ... } yazmak bu işi her seferinde baştan yaptırır. Bu yüzden .NET 8 ile gelen CA1869 analyzer'ı bu kullanımı işaretler. Options'ı static bir field'da ya da dependency injection ile tek bir instance olarak tut. İlk kullanımdan sonra instance kilitlenir; değiştirmeye çalışırsan exception alırsın.

ASP.NET Core'da ayarları tek yerden ver: minimal API'ler için ConfigureHttpJsonOptions, controller'lar için AddJsonOptions. Uygulamanın kendi HttpClient çağrılarında, background job'larında ve testlerinde aynı ayarları kullanmazsan, API'nin yazdığını Worker'ın okuyamadığı hatalarla karşılaşırsın; yukarıdaki camelCase örneği tam olarak bu.

Hata mesajlarını istemciye olduğu gibi döndürme. STJ'nin mesajları .NET tip adlarını içeriyor (Bookstore.IO.Json.Payments.PaymentRequest); log'a yaz, istemciye path bilgisiyle sade bir doğrulama hatası dön.

Contract'ı sürümle. Strict ayarlar yeni bir alanı reddettiği için, istemciler ve service'ler aynı anda deploy edilmiyorsa yeni alanları önce okuyucu tarafta tanımla, sonra yazıcı tarafta göndermeye başla.

Sık Düşülen Tuzaklar

Newtonsoft.Json alışkanlıkları. Newtonsoft varsayılan olarak harf duyarsız eşleşir ve tırnak içindeki sayıları kabul eder; STJ'nin varsayılanları ikisini de yapmaz. Bir projeyi STJ'ye taşırken testlerin geçip üretimde alanların boş gelmesinin en sık sebebi bu. Web ayarlarını kullan ya da farkı bilerek ayarla.

Tip adını JSON'da taşımak. Newtonsoft'un TypeNameHandling ayarı payload'daki $type alanına göre herhangi bir .NET tipini yaratabildiği için yıllardır remote code execution açıklarının kaynağı. STJ'nin polymorphism'i yalnız [JsonDerivedType] ile izin verdiğin tipleri yaratır; bu tasarımı koru ve tip adı yerine sabit discriminator'lar kullan. Insecure deserialization'ın ayrıntısı OWASP (Open Web Application Security Project; web güvenliği üzerine çalışan açık topluluk) Top 10 serisinin Deserialization ve Mass Assignment yazısında.

Duplicate property'yi zararsız sanmak. Tek bir service'te öyle görünebilir; araya bir gateway, bir WAF (Web Application Firewall; HTTP trafiğini kurallara göre süzen güvenlik katmanı) ya da başka dilde yazılmış bir service girdiği anda iki bileşen aynı isteği farklı okuyabilir. Dışarıdan gelen payload'larda AllowDuplicateProperties = false.

Polymorphic base type'ı concrete bırakmak. Discriminator gelmezse STJ sessizce base type'ı yaratır ve alt tipin alanları kaybolur. Base type'ı abstract yap.

Para ve tarih biçimini culture'a bırakmak. Converter'larda ve ToString çağrılarında her zaman CultureInfo.InvariantCulture.

Küçük Bir Deneme

★☆☆ PaymentStatus enum'una Failed değerini Authorized ile Captured arasına ekle. Enum'u sayı olarak serialize eden ayarlarla eski bir JSON'u okuyup sonucun nasıl kaydığını gör; sonra JsonStringEnumConverter ile tekrarla.

★★☆ PaymentMethod hiyerarşisine GiftCardPayment(string Code) ekle. Yeni tipi tanımayan eski bir istemcinin (attribute listesinde "gift-card" olmayan bir kopya) bu JSON'u okuduğunda ne aldığını gör ve bilinmeyen tipleri bir UnknownPayment'a düşürmenin yollarını araştır.

★★★ Bir TypeInfoResolver modifier'ı yaz: [SensitiveData] ile işaretli property'leri çıkarmak yerine değerlerini maskele ("+90 *** *** ** 00"). İpucu: JsonPropertyInfo.Get delegate'ini sarmalayabilirsin.

Bu yazıdaki her şey reflection tabanlı metadata ile çalıştı. Bir sonraki yazıda source generation'a geçiyoruz: JsonSerializerContext ile başlangıç süresini ve allocation'ı ölçeceğiz, JsonDocument, JsonNode ve Utf8JsonReader arasında seçim yapacağız ve 1 milyon siparişlik bir JSON export'unu belleğe almadan okuyacağız.