İlkay İlknur

System.Text.Json API'larına .NET 5.0 İle Beraber Gelen Yenilikler

October 25, 2020

Önceki yazılarımdan birinde .NET Core 3.0 ile beraber gelen yeni System.Text.Json API'larından bahsetmiştim. O yazıyı, bu API'ların mevcutta kullandığımız Newtonsoft.Json kütüphanesine göre oldukça yeni olduğundan bu nedenle de özellik bakımından arada farklar olabileceğinden, .NET 5.0 ile beraber bu farkın biraz daha kapanacağından bahsederek sonlandırmıştım. Bu yazının konusu da System.Text.Json API'larına .NET 5.0 ile beraber gelen yenilikler. Vakit kaybetmeden hızlıca yenilikleri inceleyelim.

String'den farklı bir tipte key'e sahip Dictinary desteği

.NET 5.0 öncesinde key tipi stringden farklı olan dictionaryleri serialize & deserialize etmek istediğimizde NotSupportedException alıyorduk. .NET 5.0 ile beraber artık bu limitasyon ortadan kalkıyor.

Örnek yaparsak,

var dictionary = new Dictionary<intstring>()
{
    { 34, "istanbul" },
    { 6,"Ankara" },
    { 1,"Adana"}
};
 
var json = JsonSerializer.Serialize(dictionary);
//json: {"34":"istanbul","6":"Ankara","1":"Adana"}
var dictionary2 = JsonSerializer.Deserialize<Dictionary<intstring>>(json);

Fieldlar için serialize & deserialize desteği

.NET 5.0 öncesindeki eksikliklerden biri de fieldların serialize & deserialize edilmesi eksikliğiydi. .NET 5.0 ile beraber artık fieldlarında serialize & deserialize edilmesini sağlayabiliyoruz. Ancak bu özellik default olarak açık değil. Bu yüzden JsonSerializerOptions içerisindeki IncludeFields propertysini true'ya çekmek gerekiyor.

Örnek yaparsak,

var plaka = new Plaka
{
    Kod = 1,
    Sehir = "Adana"
};
var options = new JsonSerializerOptions
{
    IncludeFields = true
};
 
var json = JsonSerializer.Serialize(plaka, options);
//json: {"Kod":1,"Sehir":"Adana"}
var plaka2 = JsonSerializer.Deserialize<Plaka>(json, options);
 
public class Plaka
{
    public int Kod;
    public string Sehir;
}

Immutable class & structlar için serialize & deserialize desteği

Immutable olarak tanımlanan class ve structların serialize & deserialize desteği de yine bir önceki versiyondaki eksikliklerden biriydi. Aşağıdaki gibi bir classımız olduğunu düşünelim.

public class Plaka
{
    public int Kod { get; }
    public string Sehir { get; }
 
    public Plaka(int kod, string sehir)
    {
        Kod = kod;
        Sehir = sehir;
    }
 
    public Plaka()
    {
 
    }
}

JsonSerializer bir jsonı deserialize etmek istediğinde parametresiz constructorı kullanacağı için hiçbir zaman doğru bir şekilde deserialize işlemi yapamayacaktı. .NET 5.0 ile beraber gelen JsonConstructor attribute'ü ile beraber JsonSerializer'a kullanması gereken constructorı bildirip doğru bir şekilde deserialize etmesini sağlayabiliyoruz.

var plaka = new Plaka(1, "Adana");
 
var json = JsonSerializer.Serialize(plaka);
//json: {"Kod":1,"Sehir":"Adana"}
var plaka2 = JsonSerializer.Deserialize<Plaka>(json);
public class Plaka
{
    public int Kod { get; }
    public string Sehir { get; }
     
    [JsonConstructor]
    public Plaka(int kod, string sehir)
    {
        Kod = kod;
        Sehir = sehir;
    }
 
    public Plaka()
    {
 
    }
}

Eğer class içerisinde sadece bir tane parametreli constructor varsa JsonSerializer JsonConstructor attribute'ünü belirtmeye gerek kalmadan otomatik olarak o constructorı kullanır.

public class Plaka
{
    public int Kod { get; }
    public string Sehir { get; }
     
    public Plaka(int kod, string sehir)
    {
        Kod = kod;
        Sehir = sehir;
    }
}

Structlarda ise her zaman parametresiz constructor bulunduğu için structlar için mutlaka yukarıda bahsettiğimiz attribute'ü kullanmak zorundayız.

Peki JsonSerializer parametreli constructor içerisinde hangi parametreye hangi değeri göndereceğini nereden biliyor diye düşünebilirsiniz. JsonSerializer property adını otomatik olarak camelCase'e çeviriyor ve constructor içerisinde o parametreyi arıyor. Eğer bulursa ilgili parametreye ilgili değeri gönderiyor. Bulamadığı durumlarda InvalidOperationException alabilirsiniz.

Reference Handling

.NET 5.0 öncesinde circular referansları yönetmede kullanılan tek yöntem bir MaxDepth belirtip, bu limit geçildiğinde exception fırlatılmasıydı. .NET 5.0 ile beraber JsonSerializerOptions içerisine eklenen ReferenceHandling propertysini ReferenceHandling.Preserve set ettiğimizde artık serializer $id, $values ve $ref gibi metadata propertylerini de json içerisine yazarak arrayler, collectionlar vb.. gibi yerlerde referans vermek için kullanabilmekte.

var employee = new Employee
{
    Name = "osman",
    Reports = new List<Employee>()
};
var employee2 = new Employee
{
    Name = "mehmet",
    Manager = employee
};
var employee3 = new Employee
{
    Name = "ahmet",
    Manager = employee
};
 
employee.Reports.AddRange(new[] { employee2, employee3 });
 
var json = JsonSerializer.Serialize(employee, new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.Preserve
});
public class Employee
{
    public string Name { getset; }
    public Employee Manager { getset; }
    public List<Employee> Reports { getset; }
}

JsonSerializer'dan çıkan JSON

{
  "$id""1",
  "Name""osman",
  "Manager"null,
  "Reports": {
    "$id""2",
    "$values": [
      {
        "$id""3",
        "Name""mehmet",
        "Manager": {
          "$ref""1"
        },
        "Reports"null
      },
      {
        "$id""4",
        "Name""ahmet",
        "Manager": {
          "$ref""1"
        },
        "Reports"null
      }
    ]
  }
}

NumberHandling

Bazı durumlarda JSON içerisinde number değerler tırnak içerisinde yani string olarak yer alabilmekte. Böyle durumlarda bu alanlar deserialize edilirken int, double gibi değişkenlere deserialize edilemiyordu. .NET 5.0 ile beraber JsonSerializerOptions içerisindeki NumberHandling propertysi ile bu alanların serialize ve deserialize edilirkenki davranışlarını kontrol edebiliyoruz.

JsonNumberHandling enumı içerisinde kullanabileceğimiz dört değer bulunmakta.

Değer Açıklaması
Strict Number valuelar json içerisinde number olarak tanımlanan alanlardan okunur ve yazılır.(Tırnak olmadan.)
AllowReadingFromString Number alanlar JSON içerisinde hem number alanlardan hem de string olarak belirtilen alanlardan okunabilir.
WriteAsString Number alanlar JSON string olarak yazılır.
AllowNamedFloatingPointLiterals "NaN", "Infinity", ve "-Infinity" gibi floating point constantları string olarak okunup Single.NaN gibi değerlere set edilebilir. Aynı zamanda bu constant değerler JSON'a serialize edilirken string olarak serialize edilir.

Serialize İşlemi Sırasında Default Değerleri Yoksayma

.NET 5.0 ile beraber default değere sahip olan alanların serialize edilirken JSON içerisinde yer almamasını sağlayabilirsiniz. Bunun için JsonSerializerOptions içerisindeki DefaultIgnoreCondition propertysini kullanabilirsiniz. Bu propertynin alabileceği değerler ise şu şekilde.

Değer Açıklaması
Never Property her zaman serialize ve deserialize edilir.
Always Property her zaman ignore edilir.
WhenWritingDefault Eğer property default değere eşitse serialize edilirken ignore edilir.
WhenWritingNull Eğer property null ise ignore edilir. Bu alan reference type değişkenler ve propertyler için kullanılır.

HttpClient JSON Extension Metotları

.NET 5.0 ile beraber HttpClient içerisine JSON extension metotları geliyor. Metotların ne iş yaptığına zaten çok değinmeyeceğim. İsimlerinden oldukça anlaşılır durumda. Extension metotlar System.Net.Http.Json namespace'i içerisinde bulunmakta.

Metotların isimleri şunlar,

  • GetFromJsonAsync
  • PostAsJsonAsync
  • PutAsJsonAsync

C# 9.0 Record Desteği

C# 9.0 ile beraber gelen en önemli yeniliklerden biri de recordlar. .NET 5.0 ile beraber recordların serialize & deserialize edilmesi de mümkün.

Performans İyileştirmeleri

.NET 5.0 ile beraber mevcut JsonSerializer API'larının performanslarında da iyileştirmeler yapılmış durumda. Özellikle büyük collectionların serialize & deserialize edilmesinde güzel iyileştirmeler bulunmakta. Bunlarla ilgili issueları incelemek için aşağıdaki linkleri kullanabilirsiniz.

Bir sonraki yazıda görüşmek üzere