Mert Özen Her Satırda Daha İleri
Geliştirici Araçları JSON, YAML ve Configuration Formatları — #01

JSON'u Gerçekten Bilmek: RFC 8259, Sayı Hassasiyeti, Duplicate Key, JSONC, JSON5 ve JSON Lines

Mert Özen 15 Kas 2026 16 dk 5 görüntülenme
JSON'u Gerçekten Bilmek: RFC 8259, Sayı Hassasiyeti, Duplicate Key, JSONC, JSON5 ve JSON Lines

RFC 8259 duplicate key'i ve sayı hassasiyetini tanımsız bıraktığı için aynı JSON'u Node.js, Python, jq ve .NET farklı okuyabiliyor: dördü de tekrar eden key'de sonuncuyu alıyor, 2^53'ten büyük ID'yi yalnız JavaScript yuvarlıyor. BOM, NaN, baştaki sıfır, appsettings.json'daki JSONC, JSON5 ve Bookstore sipariş export'unun JSON Lines hâlini gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.404), Node.js 26.13, Python 3.14, jq 1.8.2 ve json5 2.2.3 ile çalıştırılmıştır. JSON spec'i olarak Aralık 2017 tarihli güncel standart esas alınmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) sipariş servisinden gelen bir kaydı dört farklı araçla okuyacağız. Dosya kısa ama iki tuzak taşıyor: 2^53'ten büyük bir sipariş numarası ve iki kez yazılmış bir status key'i.

{
  "orderId": 9007199254740993,
  "status": "Paid",
  "total": 24.90,
  "status": "Cancelled"
}

Node.js'te JSON.parse, Python'da json.load, komut satırında jq ve .NET'te System.Text.Json ile bu dosyayı okuyunca orderId, status ve total ne çıkar? Dördü aynı sonucu verir mi? Bir de şunu düşün: .NET'te aynı dosyayı appsettings.json okur gibi configuration provider'a verirsen ne olur?

RFC 8259 Ne Söylüyor?

JSON'un bugünkü tanımı, RFC (Request for Comments; standart ve öneri dokümanları) serisindeki 8259 numaralı doküman. Bu seriyi internet standartlarını yazan IETF (Internet Engineering Task Force; internet protokollerini standartlaştıran kurul) yayımlıyor. RFC 8259 Aralık 2017'de yayımlandı ve Internet Standard statüsünde (STD 90). Spec kısa: dört primitive tip (string, number, boolean, null) ve iki yapısal tip (object, array). Bir JSON metni herhangi bir değer olabiliyor; eski RFC 4627'deki "en dışta object ya da array olmalı" kuralı kalktı, yani tek başına 42 ya da "Dune" da geçerli bir JSON metni.

String tarafında kural net: çift tırnak, ters bölü ve U+0000–U+001F arasındaki kontrol karakterleri mutlaka escape edilir, diğer her karakter isteğe bağlı olarak \uXXXX ile yazılabilir. "İsteğe bağlı" kısmı, serializer'dan serializer'a farklı çıktı demek:

$ jq -cn --arg title "Çalıkuşu" --arg note $'The "Hobbit"\tdraft' '{title: $title, note: $note}'
{"title":"Çalıkuşu","note":"The \"Hobbit\"\tdraft"}
$ python -c 'import json; print(json.dumps({"title": "Çalıkuşu"}))'
{"title": "\u00c7al\u0131ku\u015fu"}
$ dotnet run escape.cs
{"Title":"\u00C7al\u0131ku\u015Fu","Note":"The \u0022Hobbit\u0022\tdraft"}
{"Title":"Çalıkuşu","Note":"The \u0022Hobbit\u0022\tdraft"}

Bu çıktıların hepsi geçerli ve aynı değeri taşıyor. jq Türkçe harfleri olduğu gibi yazıyor; Python'un json.dumps'ı varsayılan olarak ASCII (İngilizce harf, rakam ve temel işaretlerden oluşan 128 karakterlik set) dışını escape ediyor (ensure_ascii=False ile kapanır); .NET ise varsayılan ayarla hem Türkçe harfleri hem de çift tırnağı " olarak yazıyor. Sebebi .NET'in varsayılan encoder'ının, HTML içine gömülebilecek JSON'u güvenli tutmak için temkinli davranması. İkinci satırdaki encoder harfleri olduğu gibi bırakıyor ama HTML açısından riskli karakterleri escape etmeye devam ediyor:

#:property PublishAot=false
using System.Text.Encodings.Web;
using System.Text.Json;

var book = new { Title = "Çalıkuşu", Note = "The \"Hobbit\"\tdraft" };

Console.WriteLine(JsonSerializer.Serialize(book));

// ASCII dışı harfler olduğu gibi yazılır; HTML açısından riskli karakterler escape edilmeye devam eder
JsonSerializerOptions relaxed = new() { Encoder = JavaScriptEncoder.Create(System.Text.Unicode.UnicodeRanges.All) };
Console.WriteLine(JsonSerializer.Serialize(book, relaxed));

Bu fark bir log'da Türkçe bir kitap adını grep ile ararken ya da iki servisin ürettiği JSON'u byte byte karşılaştırırken ortaya çıkar. Encoder ayarlarının ayrıntısı .NET'te I/O İşlemleri serisinin System.Text.Json yazılarında.

Encoding konusunda RFC net: kapalı bir sistemin dışına giden JSON metni UTF-8 olmak zorunda ve gönderen taraf başa BOM (Byte Order Mark; dosyanın başına konan ve encoding'i işaretleyen görünmez karakter) eklememeli. Okuyan taraf ise BOM'u görmezden gelebilir; "gelebilir", "gelmeli" değil. Sonucunu aşağıda göreceğiz.

Spec'in Söylemedikleri: Duplicate Key ve Sayı Hassasiyeti

RFC 8259 bir object içindeki adların benzersiz olmasını SHOULD ile istiyor, yani öneriyor ama zorunlu tutmuyor. Aynı ad iki kez geçtiğinde ne olacağını da tanımlamıyor; yalnızca davranışın öngörülemez olduğunu, bazı parser'ların son değeri aldığını, bazılarının hata verdiğini söylüyor. Sayılarda da durum benzer: spec aralık ve hassasiyet sınırı koymuyor, yalnızca IEEE'nin (Institute of Electrical and Electronics Engineers; elektrik ve elektronik mühendislerinin standart kuruluşu) 754 standardındaki double'ı kullanan yazılımların yaygın olduğunu ve -(2^53)+1 ile 2^53-1 arasındaki tam sayıların her yerde aynı okunacağını not ediyor. Tahmindeki .NET programı şu:

#:package Microsoft.Extensions.Configuration.Json@10.0.15
#:property PublishAot=false
using System.Text.Json;
using Microsoft.Extensions.Configuration;

string json = File.ReadAllText("order.json");

// 1) Web varsayılanlarıyla System.Text.Json
Order order = JsonSerializer.Deserialize<Order>(json, JsonSerializerOptions.Web)!;
Console.WriteLine($"Serializer: {order.OrderId} {order.Status} {order.Total}");

// 2) Aynısı, duplicate key reddedilerek (.NET 10)
JsonSerializerOptions strict = new(JsonSerializerOptions.Web) { AllowDuplicateProperties = false };
try
{
    JsonSerializer.Deserialize<Order>(json, strict);
}
catch (JsonException ex)
{
    Console.WriteLine($"Strict: {ex.Message}");
}

// 3) Configuration JSON provider'ı (appsettings.json böyle okunur)
try
{
    IConfiguration config = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("order.json")
        .Build();
    Console.WriteLine($"Configuration: {config["status"]}");
}
catch (InvalidDataException ex)
{
    Console.WriteLine($"Configuration: {ex.InnerException?.Message}");
}

record Order(long OrderId, string Status, decimal Total);

Dört aracın sonucu:

$ node read-order.mjs
9007199254740992 Cancelled 24.9
{"orderId":9007199254740992,"status":"Cancelled","total":24.9}
$ python read_order.py
9007199254740993 Cancelled 24.9
{"orderId": 9007199254740993, "status": "Cancelled", "total": 24.9}
$ jq -c . order.json
{"orderId":9007199254740993,"status":"Cancelled","total":24.90}
$ dotnet run read-order.cs
Serializer: 9007199254740993 Cancelled 24.90
Strict: Duplicate property 'status' encountered during deserialization of type 'Order'.
Configuration: A duplicate key 'status' was found.

Duplicate key'de dördü de varsayılan olarak sonuncuyu aldı: sipariş "Paid" değil "Cancelled" görünüyor ve hiçbiri uyarı vermedi. Bu, iki farklı sistemin aynı mesajı farklı yorumlaması için yeterli. Parser'lar arasındaki bu tür farklar zaman zaman güvenlik açığı olarak da raporlanıyor, çünkü doğrulamayı yapan katman bir değeri, işi yapan katman başka bir değeri görebiliyor. .NET 10'daki AllowDuplicateProperties = false serializer'ı katı moda alıyor. Configuration provider ise zaten katı: aynı key'i bir appsettings.json'da iki kez yazarsan uygulama açılırken FormatException alırsın.

Sayıda ise Node.js ayrışıyor: JavaScript'in tek number tipi double olduğu için 9007199254740993 sessizce 9007199254740992 oldu. Bunun bit düzeyindeki açıklaması Number System'ler ve Data Representation serisinin Integer Temsili yazısında; burada format açısından iki nokta önemli. Birincisi total: Node ve Python 24.90'ı 24.9 olarak geri yazdı. Değer aynı ama metin değişti; .NET decimal ve jq ise orijinal metni korudu. İkincisi jq 1.8'in davranışı: sayının orijinal metnini saklıyor, ama üzerinde işlem yaptığın anda double'a çeviriyor:

$ jq '.orderId, .orderId + 1' order.json
9007199254740993
9007199254740992

JavaScript tarafında çözüm ya ID'yi API'de string olarak taşımak ya da ES2026 ile standarda giren reviver'ın üçüncü parametresini kullanmak. Bu parametre, değerin JSON'daki ham metnini verir:

import { readFileSync } from "node:fs";

const text = readFileSync("order.json", "utf8");

// reviver'ın üçüncü argümanı orijinal metni taşır (ES2026 JSON.parse source text access)
const order = JSON.parse(text, (key, value, context) =>
  key === "orderId" ? BigInt(context.source) : value);

console.log(order.orderId, typeof order.orderId);
console.log(JSON.stringify({ orderId: order.orderId.toString() }));
$ node big-id.mjs
9007199254740993n bigint
{"orderId":"9007199254740993"}

Kalıcı çözüm sözleşmede: 2^53'ü aşabilecek ID'leri (Snowflake tipi ID'ler, bazı veritabanlarının bigint kolonları) API'de string olarak taşı. Para tutarlarını da double ile işleme; .NET'te decimal, Python'da parse_float=Decimal kullan.

Tarih, NaN ve "0012": JSON'da Olmayan Değerler

JSON'da tarih tipi yok. Yaygın anlaşma, tarihi ISO'nun (International Organization for Standardization; uluslararası standart kuruluşu) 8601 standardına uygun bir string olarak taşımak; .NET, UTC (Coordinated Universal Time; eşgüdümlü evrensel saat) olan bir DateTime'ı "2026-11-14T09:30:00Z" biçiminde yazar. Bu yalnızca bir gelenek: alıcı taraf o string'in tarih olduğunu şemadan ya da sözleşmeden bilir, JSON'un kendisinden değil. Saat dilimini her zaman yaz; Z'siz bir zaman damgası okuyanın yerel saatine göre yorumlanabilir.

NaN ve Infinity'nin JSON'da karşılığı yok, spec bunları açıkça dışarıda bırakıyor. Araçlar bu boşluğu farklı dolduruyor:

$ python -c 'import json; print(json.dumps({"avgRating": float("nan")}))'
{"avgRating": NaN}
$ node -e 'console.log(JSON.stringify({ avgRating: NaN }))'
{"avgRating":null}
$ dotnet run nan.cs
.NET number values such as positive and negative infinity cannot be written as valid JSON. To make it work when using 'JsonSerializer', consider specifying 'JsonNumberHandling.AllowNamedFloatingPointLiterals' (see https://learn.microsoft.com/dotnet/api/system.text.json.serialization.jsonnumberhandling).
{"AvgRating":"NaN"}

Python geçersiz JSON üretiyor (NaN çıplak bir kelime), JavaScript değeri sessizce null'a çeviriyor, .NET ise exception fırlatıyor ve istersen "NaN" string'i olarak yazıyor. Bookstore'da henüz hiç puan almamış bir kitabın ortalama puanı NaN olabilir; o zaman en açık sözleşme, alanı null göndermek ya da hiç göndermemek.

Baştaki sıfır da yasak: 0012 geçerli bir JSON sayısı değil. Asıl sorun, böyle değerlerin hiç sayı olmaması gerektiği. Sipariş kodu, posta kodu, ISBN (kitaplara verilen uluslararası standart numara) ya da telefon numarası "sayıya benzeyen string"dir; sayı olarak taşındığında baştaki sıfır kaybolur:

$ echo '{"code": 0012}' | python -c 'import json, sys; json.load(sys.stdin)' 2>&1 | tail -1
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 1 column 11 (char 10)
$ echo '{"code": 0012}' | node -e 'JSON.parse(require("node:fs").readFileSync(0, "utf8"))' 2>&1 | grep SyntaxError
SyntaxError: Unexpected number in JSON at position 10 (line 1 column 11)
$ echo '{"code": 0012, "avgRating": NaN}' | jq -c .
{"code":12,"avgRating":null}

Python ve Node reddediyor. jq 1.8.2 ise hem 0012'yi hem NaN'ı hata vermeden kabul edip 12 ve null yazıyor. Yani jq'nun bir dosyayı okuyabilmesi, o dosyanın geçerli JSON olduğunu göstermez; doğrulama için katı bir parser ya da JSON Schema kullan (serinin beşinci yazısı).

BOM: Dosyanın Başındaki Görünmeyen Üç Byte

Windows'ta bazı editörler ve Windows PowerShell 5.1'in -Encoding UTF8 parametresi, UTF-8 dosyanın başına EF BB BF byte'larını ekler. Ekranda görünmez, ama parser görür:

$ printf '\xef\xbb\xbf{"isbn": "9780441013593", "stock": 12}\n' > stock-bom.json
$ xxd stock-bom.json | head -1
00000000: efbb bf7b 2269 7362 6e22 3a20 2239 3738  ...{"isbn": "978
$ node -e 'JSON.parse(require("node:fs").readFileSync("stock-bom.json", "utf8"))' 2>&1 | grep SyntaxError
SyntaxError: Unexpected token '', "{"isbn": "... is not valid JSON
$ python -c 'import json; json.load(open("stock-bom.json", encoding="utf-8"))' 2>&1 | tail -1
json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)
$ jq -c . stock-bom.json
{"isbn":"9780441013593","stock":12}

Node.js ve Python'un json modülü reddetti, jq kabul etti. Python hata mesajında çözümü de söylüyor: utf-8-sig encoding'i BOM'u atlar. .NET'te sonuç dosyayı nasıl okuduğuna bağlı:

#:property PublishAot=false
using System.Text.Json;

// File.ReadAllText BOM'u tanır ve atar
using (JsonDocument doc = JsonDocument.Parse(File.ReadAllText("stock-bom.json")))
{
    Console.WriteLine($"ReadAllText: stock={doc.RootElement.GetProperty("stock")}");
}

// stream: serializer BOM'u kendisi atlar
await using (FileStream stream = File.OpenRead("stock-bom.json"))
{
    JsonElement root = await JsonSerializer.DeserializeAsync<JsonElement>(stream);
    Console.WriteLine($"Stream: stock={root.GetProperty("stock")}");
}

// ham byte'lar: BOM parser'a ulaşır
try
{
    using JsonDocument doc = JsonDocument.Parse(File.ReadAllBytes("stock-bom.json"));
}
catch (JsonException ex)
{
    Console.WriteLine($"ReadAllBytes: {ex.Message}");
}
$ dotnet run bom.cs
ReadAllText: stock=12
Stream: stock=12
ReadAllBytes: '0xEF' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.

File.ReadAllText BOM'u tanıyıp atıyor, serializer stream'den okurken de atlıyor; ama byte dizisini doğrudan JsonDocument.Parse'a verirsen BOM parser'a ulaşıyor. Bookstore'da bu, bir tedarikçinin Windows'ta hazırlayıp gönderdiği stok dosyasının bir serviste okunup diğerinde hata vermesi demek. Kural basit: ürettiğin JSON'u BOM'suz yaz (PowerShell 7'nin varsayılanı zaten BOM'suz UTF-8), dışarıdan aldığını okurken BOM'a toleranslı ol.

Yorumlu JSON: JSONC ve appsettings.json

RFC 8259'da yorum yok. Ama her gün yorumlu "JSON" yazıyoruz: tsconfig.json, Visual Studio Code'un settings.json'ı ve appsettings.json. Bu dosyaların formatı JSONC (JSON with Comments; // ve /* */ yorumlarına izin veren JSON). Editör bu dosyaları "JSON with Comments" modunda açar; dokümanına göre bu modda trailing comma da (son elemandan sonra kalan virgül) kabul ediliyor ama editör uyarı gösteriyor. Bookstore.Api'nin appsettings.json'ında ikisi de var:

{
  // Bütün environment'ların ortak ayarları; secret'lar asla buraya yazılmaz
  "ConnectionStrings": {
    "BookstoreDb": ""
  },
  "Catalog": {
    "PageSize": 20,
    "MaxPageSize": 100,
  },
  "Ordering": {
    /* Kurumsal sipariş: en az 20 adet, %10 indirim */
    "InstitutionalMinQuantity": 20,
    "InstitutionalDiscountRate": 0.10
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

.NET'in JSON configuration provider'ı bu dosyayı sorunsuz okur, çünkü içeride JsonCommentHandling.Skip ve AllowTrailingCommas ile parse eder. Aynı dosyayı varsayılan ayarlı serializer'a, jq'ya ya da başka bir dilin JSON parser'ına verdiğinde durum değişiyor:

#:package Microsoft.Extensions.Configuration.Json@10.0.15
#:package Microsoft.Extensions.Configuration.Binder@10.0.15
#:property PublishAot=false
using System.Text.Json;
using Microsoft.Extensions.Configuration;

string path = Path.GetFullPath("../../bookstore/src/Bookstore.Api/appsettings.json");

// configuration provider yorumları atlar, trailing comma'yı kabul eder
IConfiguration config = new ConfigurationBuilder().AddJsonFile(path).Build();
CatalogOptions catalog = config.GetSection("Catalog").Get<CatalogOptions>()!;
Console.WriteLine($"Configuration: PageSize={catalog.PageSize}, MaxPageSize={catalog.MaxPageSize}");

// varsayılan ayarlı serializer etmez
try
{
    JsonSerializer.Deserialize<JsonElement>(File.ReadAllText(path));
}
catch (JsonException ex)
{
    Console.WriteLine($"Serializer: {ex.Message}");
}

// provider'ın kullandığı ayarların aynısı
JsonDocumentOptions jsonc = new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true };
using JsonDocument doc = JsonDocument.Parse(File.ReadAllText(path), jsonc);
Console.WriteLine($"JsonDocument + options: {doc.RootElement.GetProperty("Ordering").GetProperty("InstitutionalDiscountRate")}");

record CatalogOptions(int PageSize, int MaxPageSize);
$ dotnet run jsonc.cs
Configuration: PageSize=20, MaxPageSize=100
Serializer: '/' is invalid after a value. Expected either ',', '}', or ']'. Path: $ | LineNumber: 1 | BytePositionInLine: 2.
JsonDocument + options: 0.10
$ jq . ../../bookstore/src/Bookstore.Api/appsettings.json
jq: parse error: Invalid numeric literal at line 2, column 5

Yani appsettings.json aslında bir JSONC dosyası. Bir CI script'i bu dosyayı jq ile okuyup bir değeri kontrol etmeye çalışırsa ilk yorumda takılır. Böyle bir script yazacaksan ya dosyada yorum kullanma ya da dosyayı JSONC'yi anlayan bir araçla oku. Yorumların bir tuzağı daha var: ayarları JSON olarak okuyup yeniden yazan bir araç (ör. bir deploy script'i) yorumları siler.

JSON5: Elle Yazılan Config İçin Genişletilmiş JSON

JSON5 (JSON'u JavaScript object syntax'ına yaklaştıran genişletilmiş format) bir adım daha ileri gidiyor ve ECMAScript 5.1'in object literal syntax'ının büyük kısmını JSON'a ekliyor: tırnaksız key, tek tırnaklı string, trailing comma, onaltılık sayı, baştaki artı işareti, sondaki ondalık nokta, yorum ve satır sonu escape'iyle bölünen string. Proje sitesine göre Chromium, Next.js ve Babel gibi projeler kullanıyor, Apple platformları da doğrudan destekliyor. Bookstore'un kargo ücretlerini tutan bir JSON5 dosyası (okuyan script json5 paketini kullanıyor; klasörde önce npm install çalıştır):

// Bookstore checkout'u için kargo ücretleri (JSON5)
{
  currency: 'TRY',
  freeShippingFrom: 500.,
  carrierCode: 0x1F,
  rates: [
    { region: 'domestic', price: +49.90 },
    { region: 'international', price: 349.90, },
  ],
  note: 'Prices include VAT; \
updated weekly',
}
$ node read-json5.mjs
{
  "currency": "TRY",
  "freeShippingFrom": 500,
  "carrierCode": 31,
  "rates": [
    {
      "region": "domestic",
      "price": 49.9
    },
    {
      "region": "international",
      "price": 349.9
    }
  ],
  "note": "Prices include VAT; updated weekly"
}

JSON5 elle yazılan ve sık düzenlenen config dosyaları için rahat. Ama iki sistem arasında veri taşımak için uygun değil: parser'ı her dilin standart kütüphanesinde yok ve JSON5 dosyası düz bir JSON parser'ına verildiğinde ilk satırda hata alırsın. Config için JSON5 seçmeden önce serinin sonraki yazılarındaki YAML (girintiye dayalı, elle yazılmaya uygun veri formatı) ve TOML (INI'ye benzeyen, tipli config formatı) ile karşılaştır.

JSON Lines: Export ve Log İçin Satır Satır JSON

JSON Lines'ın (NDJSON olarak da bilinir; her satırında ayrı bir JSON değeri bulunan metin formatı) kuralları üç maddeden ibaret: UTF-8, her satırda geçerli bir JSON değeri, satır ayırıcı olarak \n. Bookstore'un back office'i için gün sonu sipariş export'unu iki biçimde de üretelim:

#:property PublishAot=false
using System.Text.Json;

Order[] orders =
[
    new("BS-1001", "Ada", new DateTime(2026, 11, 14, 9, 30, 0, DateTimeKind.Utc), 2, 49.80m, "Paid"),
    new("BS-1002", "Alan", new DateTime(2026, 11, 14, 11, 5, 0, DateTimeKind.Utc), 1, 24.90m, "Shipped"),
    new("BS-1003", "Grace", new DateTime(2026, 11, 15, 8, 0, 0, DateTimeKind.Utc), 25, 1120.50m, "AwaitingApproval"),
];

// 1) tek bir JSON array: dosyanın tamamı tek bir değer
JsonSerializerOptions indented = new(JsonSerializerOptions.Web) { WriteIndented = true };
File.WriteAllText("orders.json", JsonSerializer.Serialize(orders, indented));

// 2) JSON Lines: her satırda bir kompakt değer, her satır \n ile biter
await using (StreamWriter writer = new("orders.jsonl"))
{
    foreach (Order order in orders)
    {
        await writer.WriteAsync(JsonSerializer.Serialize(order, JsonSerializerOptions.Web));
        await writer.WriteAsync('\n');
    }
}

// dosyanın tamamını belleğe almadan satır satır geri oku
await using FileStream stream = File.OpenRead("orders.jsonl");
await foreach (Order? order in JsonSerializer.DeserializeAsyncEnumerable<Order>(
    stream, topLevelValues: true, JsonSerializerOptions.Web))
{
    Console.WriteLine($"{order!.OrderNumber} {order.Status} {order.Total}");
}

record Order(string OrderNumber, string CustomerName, DateTime CreatedAt, int Quantity, decimal Total, string Status);
$ dotnet run export-orders.cs
BS-1001 Paid 49.80
BS-1002 Shipped 24.90
BS-1003 AwaitingApproval 1120.50
$ cat orders.jsonl
{"orderNumber":"BS-1001","customerName":"Ada","createdAt":"2026-11-14T09:30:00Z","quantity":2,"total":49.80,"status":"Paid"}
{"orderNumber":"BS-1002","customerName":"Alan","createdAt":"2026-11-14T11:05:00Z","quantity":1,"total":24.90,"status":"Shipped"}
{"orderNumber":"BS-1003","customerName":"Grace","createdAt":"2026-11-15T08:00:00Z","quantity":25,"total":1120.50,"status":"AwaitingApproval"}

DeserializeAsyncEnumerable'ın topLevelValues: true parametresi .NET 9'dan beri var ve dosyayı değer değer okuyor; yazma tarafının .NET 11'deki karşılığı .NET'te I/O İşlemleri serisinin .NET 11 yazısında. Farkı görmenin en kolay yolu, export yazılırken process'in çöktüğünü varsaymak. İki dosyayı da 330. byte'ta kesiyoruz:

$ head -c 330 orders.json > broken.json
$ head -c 330 orders.jsonl > broken.jsonl
$ jq -c '.[] | .orderNumber' broken.json
jq: parse error: Unfinished string at EOF at line 16, column 19
$ jq -c '.orderNumber' broken.jsonl
jq: parse error: Unfinished string at EOF at line 3, column 76
"BS-1001"
"BS-1002"

JSON array tek bir değer olduğu için yarım dosyadan tek sipariş bile çıkmadı. JSON Lines'ta ise yalnızca son satır bozuk; jq ilk iki siparişi yazdı ve hatayı bildirdi (stderr satırı stdout'tan önce göründü, çünkü pipe'a yazarken stdout buffer'lanıyor). Satır satır işleyen bir okuyucu bozuk satırı atlayıp devam edebilir:

import json
from decimal import Decimal

# her satır ayrı parse edilir; bozuk bir satır diğerlerini durdurmaz
with open("broken.jsonl", encoding="utf-8") as f:
    for number, line in enumerate(f, start=1):
        try:
            order = json.loads(line, parse_float=Decimal)
            print(number, order["orderNumber"], order["total"])
        except json.JSONDecodeError as e:
            print(number, "skipped:", e.msg)
$ python read_jsonl.py
1 BS-1001 49.80
2 BS-1002 24.90
3 skipped: Unterminated string starting at

Aynı nedenle JSON Lines log'lar için de doğal format: dosyanın sonuna satır eklemek için mevcut içeriği parse etmen gerekmiyor, tail -f ile izlenebiliyor, büyük dosyayı bölmek için split -l yetiyor. İki format arasındaki dönüşüm jq ile tek satır:

$ jq -c '.[]' orders.json | diff - orders.jsonl && echo "same content"
same content
$ jq -s 'length' orders.jsonl
3
$ jq -s 'map(.total) | add' orders.jsonl
1195.2
$ echo '{"orderNumber":"BS-1004","customerName":"Linus","total":24.90}' >> orders.jsonl
$ wc -l orders.jsonl
       4 orders.jsonl

-s (slurp) bütün satırları tek bir array'de toplar; bu, dosyanın tamamını belleğe almak demek, büyük export'larda dikkat. Toplamın 1195.2 çıkması da önceki bölümdeki kuralın sonucu: jq aritmetiği double ile yapar. Büyük dosyayı satır satır işlemenin .NET tarafı için .NET'te I/O İşlemleri serisinin streaming yazılarına bak.

Üretimde Dikkat

  • Sözleşmeyi yaz: tarih biçimi (ISO 8601, UTC), para (string mi decimal mi), büyük ID'ler (string), boş değer (null mı, alanın hiç olmaması mı). JSON bunları söylemiyor, API dokümanın söylemeli.
  • Gelen JSON'daki duplicate key'i bir güvenlik konusu olarak düşün. Bir gateway veya doğrulama katmanı ile asıl service farklı parser kullanıyorsa ikisi farklı değer görebilir; .NET 10'da AllowDuplicateProperties = false bu farkı kapatır.
  • Ürettiğin dosyalar BOM'suz UTF-8 olsun. Dışarıdan aldığın dosyada BOM'u tolere et, kendi pipeline'ında üretme.
  • Uzun süren export ve log'larda JSON Lines kullan: yarıda kalan bir yazma tek satırı bozar, bütün dosyayı değil.
  • jq'nun okuyabildiği her dosya geçerli JSON değildir. CI'da doğrulama gerekiyorsa katı bir parser veya şema kullan.

Sık Düşülen Tuzaklar

Birincisi, ID'yi number olarak taşıyıp JavaScript istemcide yuvarlandığını fark etmemek. Hata ancak 2^53'ü geçen ilk ID'de ortaya çıkar ve test verisinde genellikle hiç görünmez. İkincisi, para tutarını double'a okuyup tekrar yazmak: 24.90 metni 24.9 olur. İmza veya hash ile karşılaştırılan bir payload'da bu kadarı bile sorun çıkarır.

Üçüncüsü, duplicate key'i "olmaz öyle şey" diye geçmek. Elle düzenlenen büyük bir appsettings.json'da aynı bölümü iki kez yazmak kolay; .NET bunu açılışta yakalar ama Node.js tabanlı bir araç sessizce sonuncuyu alır. Dördüncüsü, "0012" gibi değerleri number'a çevirmek; Excel'den gelen veriyi JSON'a aktarırken çok sık olur. Beşincisi, Python'un json.dumps'ının NaN için geçersiz JSON üretmesi; allow_nan=False verirsen hata fırlatır.

Son olarak JSONC dosyasını JSON sanmak. tsconfig.json ve appsettings.json genel amaçlı JSON araçlarıyla okunmaya çalışıldığında ilk yorumda hata verir. Hangi dosyanın gerçekten JSON olduğunu bilmek, script yazarken zaman kazandırır.

Küçük Bir Deneme

★☆☆ order.json'daki ikinci status satırını sil ve orderId'yi string'e çevir. Dört aracın çıktısını tekrar al; hangileri artık aynı sonucu veriyor?

★★☆ export-orders.cs'e, orders.jsonl'ı AllowDuplicateProperties = false ile okuyan bir adım ekle. Bir satırda aynı key'i elle iki kez yaz ve hatanın hangi satırda olduğunu yazdır. JSON Lines'ta satır numarasını kendin tutman gerektiğini göreceksin.

★★★ 100.000 siparişlik bir export'u JSON array ve JSON Lines olarak üret. Toplam tutarı bir kez jq -s ile, bir kez satır satır jq -c ve awk ile hesapla; iki yöntemin bellek kullanımını Linux'ta /usr/bin/time -v, macOS'ta /usr/bin/time -l ile karşılaştır.

JSON'un spec'i kısa olduğu için sürprizleri spec'in boş bıraktığı yerlerden geliyor. YAML'da durum tersi: spec uzun ve sürprizler kuralların kendisinden çıkıyor. Bir sonraki yazıda YAML'ın syntax'ını, implicit typing'i ve Norveç'in ülke kodunun neden false'a dönüştüğünü gerçek parser'larla göreceğiz.