Kırıcı değişiklik (breaking change), API'yi mevcut hâliyle kullanan bir istemcinin hiçbir kod değişikliği yapmadan çalışmayı bırakmasına yol açan değişikliktir. Bu serinin dayandığı tasarım kaynağı dört tipik örneği sayar: bir alanı kaldırmak, bir alanı yeniden adlandırmak, bir alanın tipini değiştirmek ve yeni bir zorunlu alan eklemek. İsteğe bağlı yeni bir alan eklemek ise geriye uyumlu (backward compatible) kabul edilir ve aynı sürümde yapılabilir. Kırıcı değişiklik gerektiğinde yeni bir sürüm açılır.
Kural kısa ama uygulaması incelik ister: aynı değişiklik bir istemci için zararsız, başka bir istemci için kırıcı olabilir; ayrıca alan adları kadar davranışlar da sözleşmenin parçasıdır. Bu yazıda değişiklik türlerini çalışan Pydantic ve FastAPI örnekleriyle sınıflandırıyor, RFC 8259'un JSON birlikte çalışabilirlik notlarının nerede devreye girdiğini ve bir alanı kırmadan nasıl değiştirebileceğinizi gösteriyoruz. Sürüm numarasının URL'de mi başlıkta mı taşınacağı API versiyonlama yazısının konusu; serinin tamamı REST API tasarımı öğrenme rehberinde.
Hangi değişiklik kırıcıdır?

Bir değişikliğin kırıcı olup olmadığını anlamanın en pratik yolu, "v1 belgesine bakarak yazılmış bir istemci bu değişiklikten sonra da doğru çalışır mı?" diye sormaktır. Kaynakta sayılan dört durum ve uyumlu durum şöyle özetlenebilir:
| Değişiklik | Kırıcı mı? | Neden |
|---|---|---|
| Yanıttan alan kaldırmak | Evet | O alanı okuyan istemci değeri bulamaz |
| Alanı yeniden adlandırmak | Evet | Eski adı arayan istemci için alan silinmiş gibidir |
| Alan tipini değiştirmek | Evet | Sayı bekleyen kod metinle karşılaşır |
| İsteğe yeni zorunlu alan eklemek | Evet | Eski istemcilerin istekleri doğrulamadan geçemez |
| Yanıta isteğe bağlı alan eklemek | Genellikle hayır | Bilinmeyen alanı yok sayan istemci etkilenmez |
| İsteğe bağlı yeni istek alanı | Hayır | Göndermeyen istemci için varsayılan kullanılır |
Yeniden adlandırma örneğiyle somutlaştıralım. v1 yanıtı şudur:
{"id": 1, "name": "Ayşe Demir"}Alan adını full_name yapmak, eski istemci açısından name alanının silinmesi ile aynıdır; bu yüzden bu biçim ancak yeni bir sürümde, örneğin /v2/kullanicilar altında sunulabilir:
{"id": 1, "full_name": "Ayşe Demir"}Buna karşılık mevcut alanlara dokunmadan isteğe bağlı bir email alanı eklemek, kaynağın ifadesiyle geriye uyumludur ve /v1/kullanicilar adresinde kalabilir:
{"id": 1, "name": "Ayşe Demir", "email": "ayse@example.com"}İstemcinin gözünden: aynı değişiklik herkes için aynı değil
"Alan eklemek uyumludur" kuralı, istemcinin bilmediği alanları yok saydığı varsayımına dayanır. Her istemci böyle yazılmaz. Aşağıdaki örnekte bir istemci v1 yanıtını iki farklı Pydantic modeliyle okuyor: biri bilinmeyen alanları yok sayıyor (Pydantic'in varsayılanı), diğeri extra="forbid" ile reddediyor. Dört farklı sunucu yanıtının sonucu:
from pydantic import BaseModel, ConfigDict, ValidationError
class KullaniciV1(BaseModel): # istemcinin v1'e göre yazdığı model
id: int
name: str
class KatiKullaniciV1(KullaniciV1): # bilinmeyen alanı reddeden istemci
model_config = ConfigDict(extra="forbid")
yanitlar = {
"v1 aynen": {"id": 1, "name": "Ayşe"},
"opsiyonel alan eklendi": {"id": 1, "name": "Ayşe", "email": "ayse@example.com"},
"alan yeniden adlandırıldı": {"id": 1, "full_name": "Ayşe"},
"tip değişti": {"id": "usr_1", "name": "Ayşe"},
}
for model in (KullaniciV1, KatiKullaniciV1):
for ad, govde in yanitlar.items():
try:
model.model_validate(govde)
sonuc = "OK"
except ValidationError as e:
sonuc = e.errors()[0]["type"]
print(f"{model.__name__:16} {ad:26} {sonuc}")
# çıktı: KullaniciV1 v1 aynen OK
# çıktı: KullaniciV1 opsiyonel alan eklendi OK
# çıktı: KullaniciV1 alan yeniden adlandırıldı missing
# çıktı: KullaniciV1 tip değişti int_parsing
# çıktı: KatiKullaniciV1 v1 aynen OK
# çıktı: KatiKullaniciV1 opsiyonel alan eklendi extra_forbidden
# çıktı: KatiKullaniciV1 alan yeniden adlandırıldı missing
# çıktı: KatiKullaniciV1 tip değişti int_parsing
Hoşgörülü istemci yalnızca gerçekten kırıcı olan iki durumda, yeniden adlandırma (missing) ve tip değişikliğinde (int_parsing) hata verdi. Katı istemci ise isteğe bağlı alan eklenmesine de extra_forbidden ile tepki gösterdi. Bundan çıkan ders iki yönlüdür. API tarafında, belgenize "yanıtlara zamanla yeni alanlar eklenebilir, istemciler bilmedikleri alanları yok saymalıdır" cümlesini açıkça yazın; uyumluluk sözü ancak bu beklentiyle anlamlıdır. İstemci tarafında ise yanıt modellerini bilinmeyen alanları reddedecek biçimde kurmayın.
Tip değişikliği satırında ilginç bir ayrıntı da var: Pydantic'in esnek modu "1" gibi sayıya çevrilebilen bir metni tamsayı olarak kabul ederdi, ama "usr_1" çevrilemediği için hata oldu. Yani bazı tip değişiklikleri bazı istemcilerde tesadüfen çalışabilir; bu, onları uyumlu yapmaz. Sözleşme belgede yazan tiptir.
İstek tarafı: yeni zorunlu alan
Kırıcı değişiklikler yalnızca yanıtlarda olmaz. Sunucunun kabul ettiği istek gövdesine zorunlu bir alan eklemek, o alanı hiç bilmeyen eski istemcilerin tüm isteklerini doğrulama hatasına düşürür. Aynı sipariş modelinin iki hâlini karşılaştıralım:
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class SiparisZorunlu(BaseModel):
urun_id: int
adet: int
teslimat_notu: str # sonradan zorunlu yapıldı: kırıcı
class SiparisOpsiyonel(BaseModel):
urun_id: int
adet: int
teslimat_notu: str | None = None # opsiyonel eklendi: uyumlu
@app.post("/a/siparisler", status_code=201)
def olustur_a(s: SiparisZorunlu):
return s.model_dump()
@app.post("/b/siparisler", status_code=201)
def olustur_b(s: SiparisOpsiyonel):
return s.model_dump()
eski_istemci = {"urun_id": 5, "adet": 2}
c = TestClient(app)
r = c.post("/a/siparisler", json=eski_istemci)
print(r.status_code, r.json()["detail"][0]["type"], r.json()["detail"][0]["loc"])
# çıktı: 422 missing ['body', 'teslimat_notu']
r = c.post("/b/siparisler", json=eski_istemci)
print(r.status_code, r.json())
# çıktı: 201 {'urun_id': 5, 'adet': 2, 'teslimat_notu': None}
Zorunlu alan eklenen uç nokta, eski istemcinin isteğini 422 ile reddetti; FastAPI hatanın yerini ["body", "teslimat_notu"] olarak gösterdi. Aynı alanı varsayılan değerli ve isteğe bağlı eklediğimizde eski istek 201 ile kabul edildi. Kural şu: yeni istek alanları isteğe bağlı ve makul bir varsayılanla gelir; bir alanı gerçekten zorunlu yapmak gerekiyorsa bu yeni bir sürümün işidir. Doğrulama hatalarının biçimi ve 400 ile 422 farkı için istek doğrulama yazısına bakın.
Ters yön de önemlidir: sunucunun bir istek alanı için kabul ettiği değerleri daraltmak (örneğin daha önce 500 karaktere izin verilen bir alanı 100 karaktere düşürmek) eski istemcilerin geçerli isteklerini reddetmeye başlar ve bu da kırıcıdır. Kabul edilen aralığı genişletmek ise genellikle uyumludur.
Davranış değişiklikleri de sözleşmenin parçası

Tasarım kaynağı, yeni sürüm gerektiren durumları sayarken alan değişikliklerinin yanında "davranış değiştirmeyi" de anar. JSON şeması aynı kalsa bile istemcinin dayandığı davranış değişebilir. Aşağıdaki örnekler bu yazının tasarım çıkarımlarıdır; her biri, eski istemcinin yazdığı bir varsayımı bozduğu için dikkat ister:
- Durum kodunu değiştirmek: Başarılı oluşturmada 200 yerine 201 döndürmeye başlamak, yalnızca 200'ü başarı sayan bir istemciyi bozabilir. Kod seçimi için durum kodu seçimi yazısına bakın.
- Hata gövdesinin biçimini değiştirmek: Hata mesajını okuyan istemciler yeni biçimi tanımaz.
- Varsayılan değerleri değiştirmek: Varsayılan sayfa boyutunu veya sıralama yönünü değiştirmek, parametre göndermeyen istemcilerin gördüğü sonucu değiştirir.
- Enum değerlerini kaldırmak veya anlamını değiştirmek:
statusalanında"active"artık gelmiyorsa, ona göre dallanan kod çalışmaz. Yeni bir değer eklemek de, bilinmeyen değeri hata sayan istemciler için sorun olabilir; belgede "yeni değerler eklenebilir" diye belirtin. - Alanın null olabilmesi: Her zaman dolu gelen bir alanın artık
nullgelebilmesi, değeri denetlemeden kullanan istemcileri bozar. - Kimlik doğrulama veya yetki kuralını sıkılaştırmak: Daha önce izin verilen bir işlemin artık 403 dönmesi istemci açısından bir davranış değişikliğidir.
RFC 8259 açısından güvenli ve riskli değişiklikler
JSON'un kendi tanımı da bazı değişikliklerin neden güvenli, bazılarının neden riskli olduğunu açıklar. RFC 8259, nesne üyelerinin sırasını görünür kılıp kılmadıkları konusunda JSON kütüphanelerinin farklılaştığını ve davranışı üye sırasına bağlı olmayan uygulamaların bu farklardan etkilenmeyeceğini belirtir. Bu yüzden yanıttaki alanların sırasını değiştirmek bir sözleşme değişikliği sayılmamalıdır; istemciler de alan sırasına güvenmemelidir. Aynı RFC, bir nesnedeki adların benzersiz olması gerektiğini, tekrar eden adlarda alıcının davranışının öngörülemez olduğunu söyler; geçiş sırasında aynı ada iki değer koymak bu nedenle bir çözüm değildir.
Sayılar için RFC 8259 önemli bir uyarı yapar: uygulamalar sayıların aralığına ve hassasiyetine sınır koyabilir ve yaygın IEEE 754 çift hassasiyetli sayılarla çalışan yazılımların tam olarak anlaştığı tamsayı aralığı [-(2**53)+1, (2**53)-1] ile sınırlıdır. Bu aralığın dışındaki bir kimlik değeri, sayıyı kayan noktalı olarak okuyan bir istemcide sessizce değişebilir:
import json
buyuk = 2**53 + 1
metin = json.dumps({"id": buyuk})
print(metin)
# çıktı: {"id": 9007199254740993}
print(float(json.loads(metin)["id"]) == buyuk)
# çıktı: False
print(int(float(buyuk)))
# çıktı: 9007199254740992
Python tam sayıyı doğru yazdı, ama değeri kayan noktalı sayıya çeviren bir istemci 9007199254740993 yerine 9007199254740992 gördü. Kimlik alanını ileride bu aralığı aşabilecek şekilde tasarlıyorsanız, en baştan metin (string) tipinde vermek, sonradan sayıdan metne geçmenin yaratacağı kırıcı tip değişikliğini önler. Kimlik tipi seçimi alan adlandırma ve tarih formatı yazısında ele alınıyor.
Kırmadan değiştirmek: önce genişlet, sonra daralt
Bir alanı yeniden adlandırmak istediğinizde her zaman hemen yeni sürüm açmanız gerekmez. Mevcut sürümde önce yeni alanı eskisinin yanına eklersiniz; bu, isteğe bağlı alan eklemek olduğu için uyumludur:
{"id": 1, "name": "Ayşe Demir", "full_name": "Ayşe Demir"}Yeni istemciler full_name alanını kullanmaya başlar, belgede name alanı "kullanımdan kalkacak" olarak işaretlenir ve eski alan ancak bir sonraki ana sürümde kaldırılır. Böylece kırıcı adım (alanın silinmesi) tek bir planlı sürüm geçişinde toplanır ve istemcilerin geçiş için zamanı olur. Bu yaklaşımın bir maliyeti vardır: geçiş süresince iki alanın da tutarlı tutulması gerekir. Kullanımdan kaldırmanın HTTP başlıklarıyla nasıl duyurulacağı Deprecation ve Sunset yazısının konusudur.
İki sürümü tek iç modelden üretmek
Yeni sürüm açtığınızda v1 ve v2 aynı veriyi farklı biçimlerde sunar. Veriyi tek bir iç modelde tutup her sürüm için yalnızca dışa dönük temsili ayrı üretmek, iki sürümün zamanla birbirinden habersizce ayrışmasını önler:
from dataclasses import dataclass
@dataclass
class Kullanici: # iç model: veritabanındaki tek gerçek
id: int
ad: str
soyad: str
email: str | None = None
def v1_temsil(k: Kullanici) -> dict:
return {"id": k.id, "name": f"{k.ad} {k.soyad}"}
def v2_temsil(k: Kullanici) -> dict:
return {"id": k.id, "full_name": f"{k.ad} {k.soyad}", "email": k.email}
k = Kullanici(7, "Ayşe", "Demir", "ayse@example.com")
print(v1_temsil(k))
# çıktı: {'id': 7, 'name': 'Ayşe Demir'}
print(v2_temsil(k))
# çıktı: {'id': 7, 'full_name': 'Ayşe Demir', 'email': 'ayse@example.com'}
Bu düzende iş kuralları ve veritabanı erişimi bir kez yazılır; sürümler arasındaki fark, küçük ve test edilebilir dönüşüm işlevlerinde toplanır. v1 temsilinin çıktısını sabitleyen bir test yazmak, birinin v1'i yanlışlıkla değiştirmesine karşı da iyi bir sigortadır: v1 çıktısı değişirse test kırılır ve değişikliğin kırıcı olup olmadığı bilinçli olarak tartışılır.
Kırıcı değişikliği yayından önce yakalamak
Kırıcı değişikliklerin çoğu kötü niyetten değil, fark edilmemekten doğar: bir geliştirici modeldeki alan adını düzeltir, tip ipucunu sıkılaştırır ya da bir alanı varsayılansız bırakır ve bunun yayımlanmış bir sözleşmeyi değiştirdiğini düşünmez. Bunu yakalamanın en somut yolu, sözleşmenin kendisini karşılaştırılabilir bir belge olarak tutmaktır. API'niz için bir OpenAPI belgesi üretiyorsanız (FastAPI bunu otomatik yapar), her yayından önce yeni belgeyi bir öncekiyle karşılaştırmak şu soruları cevaplar: bir yol veya şema özelliği kaldırıldı mı, bir özelliğin type değeri değişti mi, istek şemasının required listesine yeni bir ad eklendi mi, bir yanıt kodu listeden çıktı mı? Bunların her biri bu yazıdaki tabloda kırıcı olarak işaretlenen bir duruma karşılık gelir.
OpenAPI 3.1.1 belirtimi ayrıca işlemler (operation) ve parametreler için deprecated alanı tanımlar; bu alan true olduğunda kullanıcıların o işlemden veya parametreden uzaklaşması beklenir. Önce genişlet, sonra daralt yaklaşımında eski parametreyi bu alanla işaretlemek, değişikliği belgeyi okuyan herkes için görünür kılar. OpenAPI belgesinin elle yazımı OpenAPI 3.1 YAML yazısında anlatılıyor. Belge karşılaştırması uyumsuzluğu gösterir, ama karar yine sizindir: değişiklik gerçekten gerekliyse yeni sürüme taşınır, değilse geri alınır.
Sık yapılan hatalar

- "Küçük bir isim düzeltmesi" diye alanı yeniden adlandırmak: Eski adı okuyan her istemci için alan silinmiştir.
- Zorunlu istek alanını mevcut sürüme eklemek: Eski istemcilerin tüm istekleri doğrulamada takılır.
- Uyumluluk beklentisini yazmamak: İstemcilere bilinmeyen alanları yok saymalarını söylemezseniz, alan eklemek bile bazılarını bozar.
- Tipi "yalnızca biraz" değiştirmek: Sayıyı metne çevirmek bazı istemcilerde tesadüfen çalışır, bazılarında çalışmaz.
- Davranış değişikliğini değişiklik saymamak: Varsayılan sıralama, durum kodu veya hata biçimi değişince şema aynı kalsa da istemci bozulur.
- Büyük sayıları sayı olarak vermek: 2**53 sınırını aşan kimlikler bazı istemcilerde sessizce değişir.
- Her değişikliğe yeni sürüm: Uyumlu değişiklikler için sürüm açmak, istemcileri gereksiz geçişlere zorlar.
Sık Sorulan Sorular
Yanıta yeni alan eklemek gerçekten güvenli mi?
İstemciler bilinmeyen alanları yok sayıyorsa evet; tasarım kaynağı da isteğe bağlı alan eklemeyi geriye uyumlu sayar. Bu beklentiyi belgede açıkça yazmak, uyumluluk sözünün temelidir.
Alanın sırasını değiştirmek kırıcı mı?
Olmamalı. RFC 8259'a göre davranışı üye sırasına bağlı olmayan uygulamalar birlikte çalışabilir; istemcilerin alan sırasına güvenmemesi gerekir.
Hata düzeltmesi kırıcı değişiklik sayılır mı?
Belgelenen davranışı düzeltmek değil, belgelenmemiş ama istemcilerin dayandığı bir davranışı değiştirmek risklidir. Bu durumda değişikliği önceden duyurmak ve gerekiyorsa geçiş süresi tanımak iyi bir uygulamadır.
Enum alanına yeni bir değer eklemek kırıcı mı?
İstemciye bağlıdır. Bilinmeyen değeri genel bir "diğer" durumu olarak ele alan istemci etkilenmez; her değeri tek tek eşleştirip eşleşmeyende hata veren istemci bozulur. Bu yüzden enum alanlarının ileride yeni değer alabileceğini ilk sürümden itibaren belgede yazmak, bu tür eklemeleri uyumlu sayabilmenin ön koşuludur.
Bir alanı ne zaman gerçekten silebilirim?
Yeni bir ana sürümde. Mevcut sürümde alan kalır, belgede kullanımdan kalkacağı yazılır, yeni sürüm yayımlandıktan sonra eski sürüm duyurulan takvimle emekliye ayrılı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 · API versiyonlama: URL ve header · Deprecation ve Sunset başlıkları · JSON alan adlandırma ve tarih formatı · İstek doğrulama: 400 ve 422