System.Text.Json vs Newtonsoft.Json: Performans, Özellikler ve Custom Converter Rehberi
.NET ekosisteminde JSON işleme denince akla iki dev gelir: Efsanevi Newtonsoft.Json (Json.NET) ve .NET Core 3.0 ile doğan yerli System.Text.Json (STJ). STJ, performans ve bellek tahsisi (alloc) açısından Newtonsoft'u açık ara geçerken, Newtonsoft hala bazı esnek özelliklerde (özellikle döngüsel referanslar, eski tip dönüşümleri) tahtını koruyor. Bu yazıda, hangi durumda hangisini seçeceğinizi, STJ ile nasıl custom converter yazacağınızı ve bilinen tuzakları ele alıyoruz.
1. Performans Karşılaştırması (Benchmark)
Aşağıdaki tablo, 10.000 kez küçük bir nesneyi serileştirip deserileştiren testin yaklaşık sonuçlarıdır (dotnet/benchmarks):
| Ölçüm | Newtonsoft.Json | System.Text.Json | Fark |
|---|---|---|---|
| Serileştirme (Önerilen) | ~350 ns | ~120 ns | ~3x hızlı |
| Deserileştirme | ~450 ns | ~180 ns | ~2.5x hızlı |
| Bellek Tahsisi (Ser.) | ~1.2 KB | ~0.4 KB | ~3x daha az alloc |
| Başlangıç Isınma | Yavaş (JIT) | Hızlı (Source Generator ile daha da hızlı) | STJ önde |
STJ Neden Daha Hızlı?
-
Utf8JsonReaderveUtf8JsonWriterdoğrudan UTF-8 baytları üzerinde çalışır, string dönüşümlerini en aza indirir. -
Span<T>veref structkullanarak ek bellek tahsisini neredeyse sıfırlar. -
Reflection yerine derleme zamanında üretilen
JsonSerializerContext(Source Generator) ile AOT dostudur.
2. Özellik Karşılaştırması (Kim Eksik?)
| Özellik | Newtonsoft.Json | System.Text.Json |
|---|---|---|
| Döngüsel Referanslar (Circular References) | ✅ PreserveReferencesHandling ile destekler |
❌ Varsayılan yok, custom converter gerekir |
| Polimorfizm (Türetilmiş tipler) | ✅ TypeNameHandling ile |
⚠️ .NET 7+ ile [JsonDerivedType] desteği geldi |
| LINQ to JSON (JObject, JArray) | ✅ (JToken) | ❌ (Yok, yerine JsonNode .NET 6+ ile geldi) |
| Kamel-case / Pascal-case dönüşümü | ✅ ContractResolver ile |
✅ JsonNamingPolicy ile |
| Null değer yok sayma | NullValueHandling.Ignore |
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull |
| Özel Tarih Formatları | DateFormatString ile |
[JsonConverter] ile veya JsonSerializerOptions.Converters |
| Büyük Veri Akışı (Streaming) | ❌ (Tümünü belleğe alır) | ✅ JsonSerializer.DeserializeAsyncEnumerable ile |
| Source Generator / AOT Desteği | ❌ | ✅ JsonSerializerContext ile |
| F# / C# 9 Record Desteği | ⚠️ (Kısmi) | ✅ Tam destek |
3. Sistem.Text.Json ile Custom Converter Yazmak (JsonConverter)
STJ'de özel serileştirme ihtiyaçları için JsonConverter<T> sınıfından türetmek gerekir. Örnek: DateTime'ı Unix timestamp (long) olarak serileştirmek.
csharp
public class UnixDateTimeConverter : JsonConverter<DateTime>
{
// Unix epoch (01.01.1970)
private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Okuyucudan long değer al (Unix timestamp)
if (reader.TokenType != JsonTokenType.Number)
throw new JsonException("Beklenen tip: Number (Unix timestamp)");
var unixTime = reader.GetInt64();
return Epoch.AddSeconds(unixTime);
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
// DateTime'ı Unix timestamp'e çevir
var unixTime = (long)(value.ToUniversalTime() - Epoch).TotalSeconds;
writer.WriteNumberValue(unixTime);
}
}
// Kullanım: Özelliğe Attribute ekleyerek
public class MyModel
{
[JsonConverter(typeof(UnixDateTimeConverter))]
public DateTime CreatedAt { get; set; }
}
// Veya global olarak ekleme:
var options = new JsonSerializerOptions();
options.Converters.Add(new UnixDateTimeConverter());
var json = JsonSerializer.Serialize(new MyModel { CreatedAt = DateTime.UtcNow }, options);
Daha Karmaşık Örnek: Polimorfik Serileştirme (Türetilmiş Sınıf)
Elimizde Animal base sınıfı ve Dog, Cat türevleri var. Serileştirirken tip bilgisini eklemek isteyelim:
csharp
[JsonConverter(typeof(AnimalConverter))]
public abstract class Animal { public string Name { get; set; } }
public class Dog : Animal { public int BarkVolume { get; set; } }
public class Cat : Animal { public int WhiskerLength { get; set; } }
public class AnimalConverter : JsonConverter<Animal>
{
public override Animal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
var typeDiscriminator = root.GetProperty("Type").GetString();
return typeDiscriminator switch
{
"Dog" => JsonSerializer.Deserialize<Dog>(root.GetRawText(), options),
"Cat" => JsonSerializer.Deserialize<Cat>(root.GetRawText(), options),
_ => throw new NotSupportedException($"Bilinmeyen tip: {typeDiscriminator}")
};
}
public override void Write(Utf8JsonWriter writer, Animal value, JsonSerializerOptions options)
{
// Tip bilgisini ekleyerek yaz
writer.WriteStartObject();
writer.WriteString("Type", value.GetType().Name);
foreach (var prop in value.GetType().GetProperties())
{
writer.WritePropertyName(prop.Name);
JsonSerializer.Serialize(writer, prop.GetValue(value), prop.PropertyType, options);
}
writer.WriteEndObject();
}
}
(Not: .NET 7+ ile bu iş için [JsonDerivedType] attribute'ü kullanmak çok daha kolaydır.)
4. System.Text.Json Source Generator (AOT ve Performans)
STJ'nin en büyük süper gücü Source Generator'dır. Reflection'ı devre dışı bırakarak, serileştirme kodunu derleme anında üretir. Bu, hem başlangıç (startup) performansını artırır hem de Native AOT derlemelerinde çalışmasını sağlar.
csharp
// 1. Kısmi bir context sınıfı oluştur:
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(MyModel))]
[JsonSerializable(typeof(List<MyModel>))]
public partial class MyJsonContext : JsonSerializerContext { }
// 2. Kullanım:
var options = new JsonSerializerOptions
{
TypeInfoResolver = MyJsonContext.Default // Context'i kullan
};
var json = JsonSerializer.Serialize(myModel, typeof(MyModel), MyJsonContext.Default);
Performans Kazancı: Reflection tabanlı serileştirmeye göre ~2x daha hızlı ve sıfır ek tahsis (alloc). Ayrıca derleme zamanında hata yakalar (ör. serileştirilemeyen tip).
5. Ne Zaman Hangi Kütüphane?
| Senaryo | Tercih |
|---|---|
| Yeni proje, yüksek performans gerekiyor, AOT hedefleniyor | System.Text.Json |
| MVC/WebAPI varsayılanı (ASP.NET Core 3.0+) | System.Text.Json (zaten varsayılan) |
| Newtonsoft ile yazılmış eski kod tabanı, geçiş zor | Newtonsoft.Json (ancak kademeli geçiş önerilir) |
| Döngüsel referanslar (circular refs) ile çalışıyorsunuz | Newtonsoft.Json veya STJ + custom ref handling |
| Dinamik JSON (JObject, JArray) manipülasyonu | Newtonsoft.Json (STJ'de JsonNode daha yeni ve sınırlı) |
| F# veya C# Record'lar ile çalışıyorsunuz | System.Text.Json (tam uyum) |
| XML, BSON, JSON Patch gibi ek formatlar | Newtonsoft.Json (ek paketlerle) |
6. Bilinen Tuzaklar ve Çözümleri (STJ)
| Tuzak | Çözüm |
|---|---|
DateTime offset bilgisi kaybolur |
DateTimeOffset kullanın veya [JsonConverter(typeof(JsonStringEnumConverter))] ile format belirtin. |
| Enum'lar sayı olarak serileşir | [JsonConverter(typeof(JsonStringEnumConverter))] ekleyin veya options'ta Converters.Add(new JsonStringEnumConverter()). |
| Büyük JSON okuma yavaşlar | JsonSerializer.DeserializeAsyncEnumerable kullanarak akış (stream) modunda okuyun. |
| Özel koleksiyonlar serileşmez | Custom converter yazın veya IEnumerable<T> dönün. |
| IgnoreCondition çalışmıyor | JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull ayarını unutmayın. |
Sonuç:
System.Text.Json, yeni .NET projelerinin varsayılanı ve geleceğidir. Newtonsoft.Json ise hala güçlüdür ancak özellikle performans ve AOT gereksinimlerinde geride kalmıştır. Custom converter yazmak, STJ'nin eksik kaldığı noktalarda (tarih formatları, polimorfizm, döngü) size tam kontrol sağlar. Eğer yeni bir projeye başlıyorsanız, STJ'yi tercih edin; Source Generator ile taçlandırın. Eski projelerde ise Newtonsoft'tan STJ'ye geçiş için adım adım (önce DTO'ları, sonra tüm katmanı) dönüşüm yapabilirsiniz.