JSON alan adları için tutarlı bir kural seçin ve her uç noktada (endpoint) uygulayın. İncelediğimiz tasarım rehberinin önerisi anahtarların snake_case yazılması (created_at, musteri_id), aynı kavramın her kaynakta aynı adla anılması ve tarihlerin RFC 3339 biçiminde, tercihen UTC olarak (2026-11-07T07:00:00Z) gönderilmesidir. Kimlik alanları ise tipini değiştirmeden, sayısal sınırlar göz önünde tutularak tasarlanır.
Bu kararların bir kısmı tasarım tercihidir, bir kısmı ise standartların açıkça yazdığı kurallardır. Bu yazıda ikisini ayrı tutuyoruz: snake_case bir gelenek, JSON'da NaN gönderememeniz ise RFC 8259'un kuralıdır. Yanıtın genel iskeleti (data ve meta) JSON yanıt yapısı yazısında, gelen verinin doğrulanması 400 ve 422 farkı yazısında ele alınıyor.
snake_case neden yaygın bir tercih?

JSON, anahtarların nasıl yazılacağı hakkında hiçbir şey söylemez; bir ad, tırnak içindeki herhangi bir metin olabilir. Yazım biçimi tamamen API tasarımcısının kararıdır. İncelediğimiz rehber snake_case önerir. Bunun pratik gerekçesi, Python gibi dillerde değişken ve alan adlarının, veritabanlarında ise sütun adlarının çoğunlukla bu biçimde yazılmasıdır; model alanı, sütun ve JSON anahtarı aynı adı taşıyınca dönüştürme katmanına gerek kalmaz. camelCase (createdAt) da yaygın bir seçenektir ve istemcisi JavaScript olan ekipler tarafından sık tercih edilir. İki biçimin birbirine teknik bir üstünlüğü yoktur; kaçınılması gereken, ikisini aynı API'de karıştırmaktır.
Karışık yazımın maliyeti istemci tarafında ortaya çıkar. Bir kaynakta created_at, ötekinde createdAt görürseniz istemci kodu her kaynak için ayrı eşleme yazmak zorunda kalır; otomatik kod üreten araçlar da tutarsız sınıf alanları üretir. Bir kural seçtikten sonra büyük-küçük harf de sabitlenmelidir: ID, Id ve id JSON için üç farklı addır. RFC 8259, ad karşılaştırmasının kod birimi kod birimi yapılmasını birlikte çalışabilir yol olarak tarif eder; yani ayrıştırıcılar bu adları birbirine eşitlemez.
Aynı kavrama aynı ad, aynı tip
Tutarlılık yalnızca yazım biçimiyle sınırlı değildir. Oluşturulma zamanı bir kaynakta created_at, başka birinde creation_date, üçüncüsünde olusturma olursa istemci üç ayrı alanı aynı kavram olarak tanımak zorunda kalır. Bir sözlük tutun: zaman damgaları için created_at ve updated_at, başka kaynağa işaret eden alanlar için musteri_id gibi _id soneki, para tutarlarında tutarın yanında birim alanı. Aşağıdaki iki gövde aynı siparişi anlatıyor; ilkinde neredeyse her alan farklı bir kuralla yazılmış:
{
"ID": 1001,
"musteriAdi": "Ayşe Yılmaz",
"Toplam-Tutar": 249.9,
"tarih": "07.11.2026 10:00",
"durum": "1"
}Sorunları sırayla sayalım: ID büyük harfle, musteriAdi camelCase, Toplam-Tutar hem büyük harf hem tire içeriyor (tireli adlar birçok dilde değişken adı olamaz, nokta gösterimiyle erişilemez), tarih yerel biçimde ve saat dilimi yok, durum ise anlamı belgesiz bir sayı içeren bir metin. Aynı sipariş düzenli bir sözlükle şöyle görünür:
{
"id": 1001,
"musteri_id": 42,
"toplam_tutar": 249.9,
"para_birimi": "TRY",
"durum": "hazırlanıyor",
"created_at": "2026-11-07T07:00:00Z",
"updated_at": "2026-11-07T07:05:30Z"
}Burada durum değeri okunabilir bir metin olarak geliyor; istemci geliştiricisinin "1" değerinin ne anlama geldiğini belgeden araması gerekmiyor. Tutar ve para birimi ayrı alanlarda; ikisini tek metinde ("249,90 TL") birleştirmek istemciyi metin ayrıştırmaya zorlardı. Müşteri adı yerine musteri_id gönderilmesi de bir seçim: ayrıntı gerekiyorsa istemci müşteri kaynağına gider.
Tarih ve saat: RFC 3339

Tarih alanları API'lerde en çok yanlış anlaşılan alanlardır. 07.11.2026 gibi bir değer Türkiye'de 7 Kasım, başka bir ülkede 11 Temmuz okunabilir. RFC 3339 bu sorunu kendi örneğiyle anlatır: 10/11/1996 biçimi farklı ülkelerde farklı yorumlandığı için küresel veri alışverişine tamamen uygunsuzdur. Çözüm, RFC 3339'un ISO 8601 üzerine tanımladığı İnternet tarih-saat profilidir. Bu profilde tam bir zaman damgası şu parçalardan oluşur:
2026-11-07: dört haneli yıl, iki haneli ay ve gün (full-date).T: tarih ile saati ayıran harf.07:00:00: saat, dakika, saniye. Saat 00 ile 23 arasındadır; ISO 8601'in izin verdiği 24 bu profilde yoktur..250: isteğe bağlı saniye kesri, nokta ve en az bir rakam.Zya da+03:00: UTC'ye göre ofset. Ofset "yerel saat eksi UTC" olarak hesaplanır;10:00:00+03:00ile07:00:00Zaynı andır.
Birkaç ayrıntı daha var. Dilbilgisi T ve Z harflerinin küçük yazılmasına da izin verir, ama RFC bu biçimi üreten uygulamaların büyük harf kullanmasını önerir (SHOULD). Saniye alanı artık saniye (leap second) anında 60 olabilir. -00:00 ofseti özel bir anlam taşır: UTC zamanı bilinip yerel ofset bilinmediğinde kullanılır ve Z ya da +00:00'dan farklı olarak "tercih edilen referans UTC'dir" iddiasında bulunmaz. Yalnızca tarih gereken alanlarda (doğum tarihi gibi) saat eklemeden full-date biçimi, yani "2026-11-07" kullanılabilir.
Neden UTC?
RFC 3339, yaz saati kuralları karmaşık olduğu ve yerel yasalarla öngörülemeyen zamanlarda değişebildiği için gerçek birlikte çalışabilirliğin en iyi UTC ile sağlandığını söyler. Ofsetsiz yerel saati ise İnternet için kabul edilemez sayar: ofset belirtilmemiş bir saat, dünyanın büyük kısmında yanlış yorumlanır. Pratik kural şudur: sunucu zamanı UTC olarak saklar ve Z ekiyle gönderir; kullanıcıya yerel saatle göstermek istemcinin işidir. Ofsetli değer göndermek de geçerlidir, ama bir alan için tek bir yaklaşım seçmek sıralama ve karşılaştırmayı kolaylaştırır.
Bu son noktanın teknik bir gerekçesi var. RFC 3339'a göre tüm değerler aynı ofsetle, aynı yazımla (hepsi Z ya da hepsi +00:00) ve aynı sayıda kesir hanesiyle yazıldığında zaman damgaları düz metin olarak sıralanabilir ve doğru zaman sırası çıkar. Ofsetler karıştığında bu özellik bozulur. Aşağıdaki örneklerde bunu ve JSON'un birkaç kuralını Python ile görüyorsunuz.
Python ve Pydantic ile tarih alanları
Pydantic 2, saat dilimi bilgisi olan bir datetime değerini JSON'a RFC 3339 uyumlu yazar; UTC için Z ekini kullanır. Saat dilimi bilgisi olmayan (naive) bir değeri ise ofsetsiz yazar ve bu, yukarıda anlatılan belirsizliği API'ye taşır. Girdi tarafında AwareDatetime tipi, ofsetsiz gelen değerleri reddeder:
from datetime import datetime, timedelta, timezone
from pydantic import AwareDatetime, BaseModel, ValidationError
class Siparis(BaseModel):
created_at: datetime
utc = datetime(2026, 11, 7, 7, 0, tzinfo=timezone.utc)
istanbul = datetime(2026, 11, 7, 10, 0, tzinfo=timezone(timedelta(hours=3)))
saatsiz = datetime(2026, 11, 7, 10, 0) # saat dilimi bilgisi yok (naive)
print(Siparis(created_at=utc).model_dump_json())
# çıktı: {"created_at":"2026-11-07T07:00:00Z"}
print(Siparis(created_at=istanbul).model_dump_json())
# çıktı: {"created_at":"2026-11-07T10:00:00+03:00"}
print(Siparis(created_at=saatsiz).model_dump_json())
# çıktı: {"created_at":"2026-11-07T10:00:00"}
print(utc == istanbul)
# çıktı: True
class SiparisGirdi(BaseModel):
teslim_at: AwareDatetime # ofsetsiz zamanı reddeder
try:
SiparisGirdi.model_validate_json('{"teslim_at": "2026-11-08T10:00:00"}')
except ValidationError as e:
print(e.errors()[0]["type"], "|", e.errors()[0]["msg"])
# çıktı: timezone_aware | Input should have timezone infoÜçüncü çıktı sık yapılan bir hatayı gösteriyor: saat dilimsiz üretilen bir zaman ofsetsiz olarak dışarı çıkar ve istemci onu hangi saat dilimine göre okuyacağını bilemez. datetime.now() argümansız çağrıldığında sunucunun yerel saatini böyle saat dilimsiz döndürür; Python 3.12'de kullanımdan kaldırılmış (deprecated) datetime.utcnow() da saat dilimi bilgisi taşımayan bir değer verir. Bunun yerine datetime.now(timezone.utc) kullanın. Dördüncü çıktı ise Z ile +03:00 değerlerinin aynı ana karşılık geldiğini doğruluyor.
Kimlik alanı: sayı mı metin mi?
id alanının tipi bir kez seçilir ve değişmez; tip değiştirmek kırıcı bir değişikliktir (bkz. breaking change). Sıralı tam sayılar kısa ve okunaklıdır. Ancak RFC 8259 sayı hassasiyeti konusunda önemli bir uyarı yapar: IEEE 754 çift hassasiyet (double) sayıları yaygın olarak kullanıldığından, ayrıştırıcıların bu hassasiyeti aşan değerleri yaklaşık temsil etmesi beklenmelidir; yalnızca -(2^53)+1 ile (2^53)-1 aralığındaki tam sayılarda uygulamaların aynı değerde anlaşacağı söylenir. Bu aralığın dışındaki bir kimlik, istemci tarafında sessizce başka bir sayıya dönüşebilir. Çok büyük ya da dağıtık sistemlerde üretilen kimlikleri bu yüzden metin olarak göndermek güvenli bir tercihtir:
{
"id": "9007199254740993",
"siparis_no": "SP-2026-000123"
}UUID gibi tanımlayıcılar zaten metindir. Baştaki sıfırların korunması gereken değerler (sipariş numarası, posta kodu) için de metin şarttır, çünkü JSON sayılarında baştaki sıfıra izin verilmez: 007 geçerli bir JSON sayısı değildir. Aynı mantık, kesinliği kritik olan değerler için de düşünülmelidir: alıcının sayıyı double ile yaklaşık temsil edebileceğini hesaba katarak para tutarlarını en küçük birim cinsinden tam sayı ya da metin olarak göndermek bir tasarım seçeneğidir.
JSON'un bilmeniz gereken kuralları

Alan tasarımında bazı kararlar size kalmaz; RFC 8259 belirler. Aşağıdaki kurallar, birlikte çalışabilir bir API için temel kabul edilmelidir:
- Adlar benzersiz olmalı (SHOULD): aynı ad bir nesnede iki kez geçerse alıcının davranışı öngörülemez. Bazı uygulamalar sonuncuyu alır, bazıları hata verir, bazıları ikisini de raporlar.
- Sabitler küçük harfli: yalnızca
true,falsevenullvardır;Trueya daNULLgeçersizdir. - NaN ve Infinity yok: JSON dilbilgisi bu değerleri temsil etmez. Ölçülemeyen bir değer için
nullkullanın ya da alanı belgeli bir biçimde atlayın. - UTF-8 zorunlu: kapalı bir ekosistem dışında değiş tokuş edilen JSON metni UTF-8 kodlanmalıdır (MUST) ve ağ üzerinden gönderilen metnin başına bayt sırası işareti (BOM) eklenmemelidir (MUST NOT).
- Dizi elemanlarının aynı tipte olması gerekmez: standart buna izin verir, ama tasarım açısından bir dizinin tek bir tip taşıması istemci kodunu basitleştirir.
- Üye sırasına güvenmeyin: RFC 8259, ayrıştırıcıların üye sırasını çağıran koda gösterip göstermediğinin değiştiğini not eder. Alan sırasına anlam yüklemeyin.
Python'un standart json modülü bu kuralların bazılarında sizi uyarmaz. Yinelenen adda sonuncuyu sessizce tutar, NaN değerini varsayılan olarak yazar (bu çıktı geçerli JSON değildir) ve büyük tam sayıları olduğu gibi gönderir; o sayının karşı tarafta double'a dönüşünce neye benzeyeceğini üçüncü örnek gösteriyor:
import json
# 1) Aynı ad iki kez: Python sonuncuyu tutar, başka ayrıştırıcılar farklı davranabilir
print(json.loads('{"rol": "okur", "rol": "admin"}'))
# çıktı: {'rol': 'admin'}
# 2) NaN geçerli JSON değildir ama Python varsayılan olarak yazar
print(json.dumps({"puan": float("nan")}))
# çıktı: {"puan": NaN}
try:
json.dumps({"puan": float("nan")}, allow_nan=False)
except ValueError as e:
print(e)
# çıktı: Out of range float values are not JSON compliant: nan
# 3) 2^53 üstü tam sayılar double ile ayırt edilemez
print(float(2**53 + 1) == float(2**53))
# çıktı: True
# 4) Farklı ofsetli zamanlar metin olarak sıralanınca yanlış sıra çıkar
print(sorted(["2026-11-07T10:00:00+03:00", "2026-11-07T08:30:00Z"]))
# çıktı: ['2026-11-07T08:30:00Z', '2026-11-07T10:00:00+03:00']Son çıktı sıralama tuzağını gösteriyor: metin olarak bakıldığında 08:30:00Z önce geliyor, oysa 10:00:00+03:00 UTC'de 07:00'dir ve aslında daha erkendir. Tüm zamanları Z ile gönderen bir API'de bu sorun oluşmaz. Türkçe karakterler konusunda ise ayrı bir endişeye gerek yok: FastAPI yanıtları UTF-8 olarak kodlar ve "Ayşe" gibi bir değer gövdede kaçış dizisine dönüşmeden yer alır.
Özet tablo
| Konu | Öneri | Dayanak |
|---|---|---|
| Anahtar yazımı | snake_case, tek biçim | Tasarım tercihi (rehber önerisi) |
| Aynı kavram | Her kaynakta aynı ad ve tip | Tasarım tercihi |
| Zaman damgası | created_at, updated_at; RFC 3339, UTC ve Z | RFC 3339 profili |
| Yalnızca tarih | 2026-11-07 (full-date) | RFC 3339 dilbilgisi |
| Büyük kimlik | 2^53 üstü ise metin | RFC 8259 sayı uyarısı |
| NaN, Infinity | Gönderilmez | RFC 8259 dilbilgisi |
| Kodlama | UTF-8, BOM yok | RFC 8259 (MUST) |
| Yinelenen ad | Kullanılmaz | RFC 8259 (SHOULD) |
Sık yapılan hatalar
- Biçim karıştırmak: bir uç noktada
user_id, ötekindeuserId. Kural seçildikten sonra kod incelemesinde denetlenmelidir. - Ofsetsiz zaman:
"2026-11-07T10:00:00"hangi saat diliminde olduğunu söylemez. Hem göndermeyin hem de girdi olarak kabul ederken ofset isteyin. - Yerel tarih biçimi:
07.11.2026ya da11/07/2026gibi değerler ülkeye göre farklı okunur. - Durumu sayı koduyla göndermek:
"durum": 3yerine"durum": "kargoda"belgesiz anlamı ortadan kaldırır. - Kimlik tipini değiştirmek:
idalanını sonradan sayıdan metne çevirmek mevcut istemcileri bozar; baştan karar verin. - Birimsiz değer:
"sure": 30saniye mi dakika mı? Birimi ada (sure_saniye) ya da belgeye yazın.
Sık Sorulan Sorular
camelCase kullanırsam yanlış mı yapmış olurum?
Hayır. JSON bir yazım biçimi dayatmaz; snake_case incelediğimiz rehberin önerisidir. camelCase seçtiyseniz onu tüm uç noktalarda, istek ve yanıt gövdelerinde ve hata yanıtlarında da uygulayın.
Unix zaman damgası (saniye sayısı) göndermek kötü mü?
Sayısal bir damga makine için açıktır ama insan için okunmaz ve birimi (saniye mi milisaniye mi) belgeye bağlıdır. RFC 3339 metni hem okunabilir hem de ofseti içinde taşır. Hangi biçimi seçerseniz seçin, bütün alanlarda aynısını kullanın.
Alan değeri yoksa null mu göndermeliyim, alanı mı atlamalıyım?
İkisi de geçerli JSON'dur ve standart bir tercih belirtmez. Önemli olan, aynı alan için her zaman aynı davranışı seçip belgelemektir. İstemci "alan yok" ile "alan null" arasında anlam farkı aramak zorunda kalmamalıdır.
Saniye kesirlerini göndermeli miyim?
Kesir isteğe bağlıdır. Göndermeye karar verirseniz hane sayısını sabit tutun; RFC 3339'un metin olarak sıralanabilirlik özelliği aynı sayıda kesir hanesi şartına bağlıdır.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: REST API tasarımı öğrenme rehberi · JSON yanıt yapısı · İstek doğrulama · Breaking change · Hata mesajı formatı