---
title: 'Doğrulama Hataları'
source: 'https://academia.sh/tr/kurslar/api-tasarimi/dogrulama-hatalari'
course: 'Web API Tasarımı'
language: tr
updated: '2026-08-17T18:06:44+00:00'
license: 'CC BY-SA 4.0'
---

# Doğrulama Hataları

Alan düzeyinde hata listesinin problem ayrıntısına eklenmesi, ilk hatada durmakla hepsini toplamanın istek turu farkı ve alan adlarının istemcideki giriş kutularıyla eşleşmesini sağlayan gövde yolu kuralı.

Bir önceki ders hata gövdesini tek biçime bağladı ama sorun kataloğundaki `dogrulama`
türünü kullanmadan bıraktı. Nedeni, bu türün diğerlerinden yapısal olarak farklı olmasıdır.
"Kopya yok" tek bir olgudur; `detail` alanındaki bir cümle onu tümüyle anlatır. "İstek
gövdesi doğrulanamadı" ise bir olgu değil, bir **olgular kümesidir**: gövdenin hangi
alanları, hangi kurallara göre reddedildi?

Ödünç servisinin ödünç verme isteği artık tek kitaplık değildir; bir üye, birden çok kalem
ve bir iade tarihi taşır. Bu ders alan düzeyinde hata bildirimini kurar ve iki kararı
ölçerek verir: hataların kaçının bir seferde bildirileceği ve alanların hangi adla
anılacağı.

## Bir Seferde Kaç Hata

Doğrulama, ilk kuralı çiğneyen alanda durabilir ya da bütün gövdeyi tarayıp hataları
toplayabilir. Aradaki fark, kullanıcının formu düzeltmek için kaç kez istek yollayacağıdır.

Aşağıdaki doğrulayıcı şemayı veri olarak alır. Şema anahtarları **gövde yolu**dur ve JSON
Pointer biçiminde yazılır: `/uye`, `/kalemler/1/isbn`. `*` işareti dizi öğeleri üzerinde
açılır, yani `/kalemler/*/isbn` kalıbı gövdedeki her kaleme uygulanır.

```js
// dogrula.mjs — govdeyi semaya gore dogrular, alan duzeyinde hata listesi uretir
// Alan adi JSON Pointer (RFC 6901): "/uye", "/kalemler/1/isbn"

export const KURALLAR = {
  zorunlu: (d) => (d === undefined || d === null || d === "" ? { kod: "zorunlu" } : null),
  desen: (d, p) => (new RegExp(p).test(String(d)) ? null : { kod: "bicim", param: p }),
  secenek: (d, p) => (p.includes(d) ? null : { kod: "secenek_disi", param: p }),
  dizi: (d, p) =>
    !Array.isArray(d) ? { kod: "dizi_degil" }
      : d.length < p.enAz ? { kod: "cok_az", param: p }
      : d.length > p.enCok ? { kod: "cok_cok", param: p }
      : null,
  tarih: (d) => (/^\d{4}-\d{2}-\d{2}$/.test(String(d)) ? null : { kod: "tarih_degil" }),
  enCokGun: (d, p, bugun) => {
    const fark = Math.round((Date.parse(d) - Date.parse(bugun)) / 86400000);
    if (Number.isNaN(fark)) return null;                 // tarih kurali zaten bildirdi
    if (fark < 0) return { kod: "gecmiste" };
    return fark > p ? { kod: "cok_ileri", param: p } : null;
  },
};

// Yol icindeki * gercek dizi dizinleriyle acilir: /kalemler/*/isbn -> /kalemler/0/isbn
const oku = (govde, yol) =>
  yol.split("/").slice(1).reduce((d, p) => (d == null ? undefined : d[p]), govde);

function yollariAc(govde, kalip) {
  const yildiz = kalip.indexOf("/*");
  if (yildiz < 0) return [kalip];
  const dizi = oku(govde, kalip.slice(0, yildiz));
  if (!Array.isArray(dizi)) return [];
  return dizi.flatMap((_, i) => yollariAc(govde, kalip.slice(0, yildiz) + `/${i}` + kalip.slice(yildiz + 2)));
}

// dogrula(sema, govde, bugun) -> [{ yol, kod, param }]
export function dogrula(sema, govde, bugun) {
  const hatalar = [];
  for (const [kalip, kurallar] of Object.entries(sema)) {
    for (const yol of yollariAc(govde, kalip)) {
      const deger = oku(govde, yol);
      for (const [ad, param] of kurallar) {
        if (ad !== "zorunlu" && (deger === undefined || deger === null || deger === "")) break;
        const sonuc = KURALLAR[ad](deger, param, bugun);
        if (sonuc) { hatalar.push({ yol, ...sonuc }); break; }  // alan basina tek hata
      }
    }
  }
  return hatalar;
}
```

İki toplama biçimini karşılaştırmak için, dört alanı birden yanlış olan bir gövde alınır ve
kullanıcının yalnızca **kendisine bildirilen** hataları düzelttiği varsayılır.

```js
// tur-tur.mjs — ilk hatada duran dogrulama ile hepsini toplayan dogrulamayi karsilastirir
import { dogrula } from "./dogrula.mjs";

const BUGUN = "2026-03-01";
const SEMA = {
  "/uye":             [["zorunlu"], ["desen", "^U-\\d{4}$"]],
  "/kalemler":        [["zorunlu"], ["dizi", { enAz: 1, enCok: 3 }]],
  "/kalemler/*/isbn": [["zorunlu"], ["desen", "^97[89]-\\d{10}$"]],
  "/kalemler/*/sube": [["zorunlu"], ["secenek", ["merkez", "sahil", "tepe"]]],
  "/iadeTarihi":      [["zorunlu"], ["tarih"], ["enCokGun", 30]],
};

// Kullanicinin ilk gonderdigi govde: dort alan birden yanlis.
const ILK = {
  uye: "1001",
  kalemler: [
    { isbn: "978-0262033848", sube: "merkez" },
    { isbn: "0262033848", sube: "deniz" },
  ],
  iadeTarihi: "2026-06-01",
};

// Kullanicinin duzeltme sirasi: hangi hata bildirilirse o duzeltilir.
const DUZELTME = {
  "/uye": (g) => (g.uye = "U-1001"),
  "/kalemler/1/isbn": (g) => (g.kalemler[1].isbn = "978-0201896831"),
  "/kalemler/1/sube": (g) => (g.kalemler[1].sube = "sahil"),
  "/iadeTarihi": (g) => (g.iadeTarihi = "2026-03-20"),
};

function turSayisi(hepsiniTopla) {
  const govde = structuredClone(ILK);
  let tur = 0;
  while (tur < 10) {
    tur++;
    const hatalar = dogrula(SEMA, govde, BUGUN);
    if (hatalar.length === 0) return tur;
    const bildirilen = hepsiniTopla ? hatalar : [hatalar[0]];
    console.log(`  tur ${tur}: ${bildirilen.map((h) => `${h.yol}=${h.kod}`).join("  ")}`);
    for (const h of bildirilen) DUZELTME[h.yol](govde);
  }
  return tur;
}

console.log("ilk hatada duran doğrulama:");
const a = turSayisi(false);
console.log(`  başarılı olana kadar istek turu: ${a}\n`);

console.log("hepsini toplayan doğrulama:");
const b = turSayisi(true);
console.log(`  başarılı olana kadar istek turu: ${b}`);
```

```
ilk hatada duran doğrulama:
  tur 1: /uye=bicim
  tur 2: /kalemler/1/isbn=bicim
  tur 3: /kalemler/1/sube=secenek_disi
  tur 4: /iadeTarihi=cok_ileri
  başarılı olana kadar istek turu: 5

hepsini toplayan doğrulama:
  tur 1: /uye=bicim  /kalemler/1/isbn=bicim  /kalemler/1/sube=secenek_disi  /iadeTarihi=cok_ileri
  başarılı olana kadar istek turu: 2
```

Beş tura karşı iki tur. Sayının kendisi gövdedeki yanlış alan sayısına bağlıdır — $n$
yanlış alan, ilk hatada duran doğrulamada $n+1$ tur demektir — ama davranış farkı sayıdan
bağımsızdır: ilk hatada durmak, kullanıcıya bir sonraki hatanın varlığını **saklar**.
Kullanıcı her düzeltmeden sonra işin bittiğini sanır ve yeni bir ret alır.

Bir ayrıntı korunmuştur: doğrulayıcı **alan başına** ilk hatada durur. `/uye` alanı hem
zorunlu hem desene uygun olmalıdır; boş bırakıldığında "zorunlu" ve "biçim" hatalarının
ikisini birden bildirmenin bir yararı yoktur, ikincisi birincinin sonucudur. Kural şudur:
alanlar arasında tarama sürer, alan içinde ilk hata yeter.

## Hata Kaydının Üç Alanı

Her hata kaydı üç şey taşır. **`yol`**, gövdedeki alanın adresidir. **`kod`**, hatanın
kararlı kimliğidir — Frontend müfredatındaki Uygulama Mimarisi kursunda kurulan doğrulama
şemalarıyla aynı ayrım: kod dallanmak içindir, ileti göstermek için. **`param`**, kodun
gerektirdiği sayıdır: hangi desen, hangi seçenekler, kaç gün.

Kodun ileti yerine geçmesi, kullanıcıya gösterilecek metni **istemci tarafında** üretmeyi
mümkün kılar. Sunucu `cok_ileri` ve `30` gönderir; istemci bunu kendi dilinde ve kendi
tonunda cümleye çevirir. Sunucu metin gönderseydi çeviri, çoğul kuralı ve ton kararı
sunucuya taşınırdı; hiçbiri sunucunun işi değildir.

Bu üçlü, problem ayrıntısına bir **ek üye** olarak eklenir. Çekirdek beş alan yerinde
kalır; `errors` dizisi onların yanına gelir.

```js
// sunucu.mjs — odunc istegini dogrulayip 422 problem ayrintisi donduren servis
import { createServer } from "node:http";
import { dogrula } from "./dogrula.mjs";

const BUGUN = "2026-03-01";                 // ornegin yinelenebilir olmasi icin sabit
const SEMA = {
  "/uye":             [["zorunlu"], ["desen", "^U-\\d{4}$"]],
  "/kalemler":        [["zorunlu"], ["dizi", { enAz: 1, enCok: 3 }]],
  "/kalemler/*/isbn": [["zorunlu"], ["desen", "^97[89]-\\d{10}$"]],
  "/kalemler/*/sube": [["zorunlu"], ["secenek", ["merkez", "sahil", "tepe"]]],
  "/iadeTarihi":      [["zorunlu"], ["tarih"], ["enCokGun", 30]],
};

// Bir onceki derste yazilan sorun katalogunun bu derse dusen tek girdisi.
let sayac = 0;
const dogrulamaSorunu = (hatalar) => ({
  type: "https://ornek.kutuphane/sorunlar/dogrulama",
  title: "İstek gövdesi doğrulanamadı",
  status: 422,
  detail: `${hatalar.length} alan kabul edilmedi.`,
  instance: `ol-${String(++sayac).padStart(4, "0")}`,
  errors: hatalar,
});

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

// Cekirdek alanlar alt alta, her hata tek satir: govde okunabilir kalsin.
const bicimle = (s) => [
  "{",
  ...["type", "title", "status", "detail", "instance"].map((a) => `  "${a}": ${JSON.stringify(s[a])},`),
  '  "errors": [',
  ...s.errors.map((h, i) => `    ${JSON.stringify(h)}${i < s.errors.length - 1 ? "," : ""}`),
  "  ]",
  "}",
].join("\n");

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const govde = JSON.parse((await govdeOku(istek)) || "{}");
  const hatalar = dogrula(SEMA, govde, BUGUN);

  if (hatalar.length) {
    yanit.writeHead(422, { "content-type": "application/problem+json; charset=utf-8" });
    return yanit.end(bicimle(dogrulamaSorunu(hatalar)));
  }
  yanit.writeHead(201, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify({ id: "O-2", uye: govde.uye, kalem: govde.kalemler.length }));
}).listen(8433, "127.0.0.1", () => console.log("dogrulama sunucusu 127.0.0.1:8433"));
```

```bash
#!/usr/bin/env bash
# Dort alani yanlis bir odunc istegi yollar, 422 govdesini gosterir.
node sunucu.mjs & sunucu=$!
sleep 0.5

curl -sS -w '\n[%{http_code}] %{content_type}\n' -X POST -H 'content-type: application/json' \
  -d '{"uye":"1001","kalemler":[{"isbn":"978-0262033848","sube":"merkez"},{"isbn":"0262033848","sube":"deniz"}],"iadeTarihi":"2026-06-01"}' \
  http://127.0.0.1:8433/odunc

echo "--- duzeltilmis govde ---"
curl -sS -w '\n[%{http_code}] %{content_type}\n' -X POST -H 'content-type: application/json' \
  -d '{"uye":"U-1001","kalemler":[{"isbn":"978-0262033848","sube":"merkez"},{"isbn":"978-0201896831","sube":"sahil"}],"iadeTarihi":"2026-03-20"}' \
  http://127.0.0.1:8433/odunc

kill "$sunucu"; wait "$sunucu" 2>/dev/null
```

```
dogrulama sunucusu 127.0.0.1:8433
{
  "type": "https://ornek.kutuphane/sorunlar/dogrulama",
  "title": "İstek gövdesi doğrulanamadı",
  "status": 422,
  "detail": "4 alan kabul edilmedi.",
  "instance": "ol-0001",
  "errors": [
    {"yol":"/uye","kod":"bicim","param":"^U-\\d{4}$"},
    {"yol":"/kalemler/1/isbn","kod":"bicim","param":"^97[89]-\\d{10}$"},
    {"yol":"/kalemler/1/sube","kod":"secenek_disi","param":["merkez","sahil","tepe"]},
    {"yol":"/iadeTarihi","kod":"cok_ileri","param":30}
  ]
}
[422] application/problem+json; charset=utf-8
--- duzeltilmis govde ---
{"id":"O-2","uye":"U-1001","kalem":2}
[201] application/json; charset=utf-8
```

Durum kodunun 400 değil **422** olması bilinçli bir seçimdir. 400, isteğin sunucu
tarafından **anlaşılamadığını** bildirir; bozuk JSON, eksik başlık, tanınmayan içerik tipi.
Buradaki gövde eksiksiz anlaşılmıştır — ayrıştırılmış, alanları okunmuş, kurallara
sokulmuştur — ve **anlamı** reddedilmiştir. Ayrım kütüğe yansıdığında da işe yarar: 400
oranındaki artış istemcinin isteği yanlış kurduğunu, 422 oranındaki artış kullanıcının
yanlış veri girdiğini gösterir; ikisinin nedeni ve çözümü farklıdır.

## Alan Adının İstemcideki Karşılığı

Hata listesinin işe yaraması, istemcinin her hatayı **hangi giriş kutusunun yanında**
göstereceğini bilmesine bağlıdır. Erişilebilir Bileşen Kalıpları kursundaki hata sunumu bu
eşleşmeyi varsayar: her hata kendi alanının ek açıklamasına bağlanır, hata özeti odağı o
alana taşır. Eşleşme kurulamazsa geriye tek bir genel ileti kalır.

Aşağıdaki ölçüm aynı dört hatayı üç ayrı sunucu sözleşmesiyle bildirir ve istemcinin
kaçını kutusuyla eşleştirebildiğini sayar.

```js
// eslesme.mjs — uc ayri alan adlandirma sozlesmesinin istemcide eslesme oranini olcer
// Istemcideki giris kutulari, sozlesmedeki kurala gore adlandirilmistir:
// ad = JSON Pointer'in bas egik cizgisi atilmis, "/" yerine "." konmus hali.
const KUTULAR = ["uye", "kalemler.0.isbn", "kalemler.0.sube", "kalemler.1.isbn", "kalemler.1.sube", "iadeTarihi"];
const adaCevir = (yol) => yol.replace(/^\//, "").replaceAll("/", ".");

// Ayni dort hata, uc ayri sunucu sozlesmesiyle bildirilmis hali.
const SOZLESMELER = {
  "alan adı yok": {
    hatalar: [{ ileti: "Gönderdiğiniz bilgilerde dört hata var." }],
    cevir: () => null,
  },
  "sunucunun iç adları": {
    hatalar: [
      { alan: "member_id", kod: "bicim" },
      { alan: "items[1].isbn", kod: "bicim" },
      { alan: "items[1].branch", kod: "secenek_disi" },
      { alan: "due_date", kod: "cok_ileri" },
    ],
    cevir: (h) => h.alan,
  },
  "gövde yolu (JSON Pointer)": {
    hatalar: [
      { yol: "/uye", kod: "bicim" },
      { yol: "/kalemler/1/isbn", kod: "bicim" },
      { yol: "/kalemler/1/sube", kod: "secenek_disi" },
      { yol: "/iadeTarihi", kod: "cok_ileri" },
    ],
    cevir: (h) => adaCevir(h.yol),
  },
};

console.log("sözleşme                    bildirilen  eşleşen  kutusunun yanında gösterilebilen");
for (const [ad, { hatalar, cevir }] of Object.entries(SOZLESMELER)) {
  const eslesen = hatalar.filter((h) => KUTULAR.includes(cevir(h)));
  console.log(
    `${ad.padEnd(27)} ${String(hatalar.length).padStart(9)}  ${String(eslesen.length).padStart(7)}  ` +
    `${eslesen.length ? eslesen.map((h) => cevir(h)).join(", ") : "hiçbiri"}`
  );
}
```

```
sözleşme                    bildirilen  eşleşen  kutusunun yanında gösterilebilen
alan adı yok                        1        0  hiçbiri
sunucunun iç adları                 4        0  hiçbiri
gövde yolu (JSON Pointer)           4        4  uye, kalemler.1.isbn, kalemler.1.sube, iadeTarihi
```

İkinci satır ilkinden daha kötüdür, çünkü aldatıcıdır: sunucu dört ayrı hata bildirmiş
görünür, alan adları da vardır, ama o adlar sunucunun kendi iç veri modelinden gelir.
`member_id`, `items[1].branch` — istemcinin gövdesinde böyle bir alan yoktur. İstemci ya
sunucunun iç adlarıyla kendi alan adları arasında elle bir çeviri tablosu tutar ya da
hataları eşleştirmeden gösterir. Çeviri tablosu tutulursa sunucunun her iç yeniden
adlandırması istemciyi bozar; yani sözleşmede olmayan bir şey sözleşme gibi davranmaya
başlar.

Kural şudur: **hata yolu, istemcinin gönderdiği gövdedeki alanın adresidir.** Sunucunun iç
model adları değil, istek gövdesinin yapısı. Bu kural iki özelliği garanti eder. Toplamdır
— gövdedeki her alanın bir yolu vardır, dizi öğeleri dâhil. Ve tek yönlü türetilebilir:
istemci gövdeyi kendisi kurduğu için yolu kutu adına çevirecek kuralı yazabilir, ters
yönde bir tablo tutmasına gerek kalmaz.

Kalan tek karar, gövde yolunun hangi biçimde yazılacağıdır. JSON Pointer tanımlı bir
standarttır, dizi dizinlerini doğal biçimde ifade eder ve kaçırma kuralları belirlidir.
Yerine nokta ayırıcı da seçilebilir; belirleyici olan biçimin kendisi değil, **belgelenmiş
ve her alan için tanımlı** olmasıdır.

## Özet

- Doğrulama ilk hatada durduğunda $n$ yanlış alan $n+1$ istek turu gerektirir; hatalar
  toplu bildirildiğinde iki tur yeter ve kullanıcıdan bir sonraki hatanın varlığı
  saklanmaz.
- Alanlar arasında tarama sürer, alan içinde ilk hata yeter: aynı alanın ardışık kural
  ihlalleri birbirinin sonucudur.
- Hata kaydı üç alan taşır — yol, kod, parametre; kullanıcıya gösterilecek metin sunucudan
  değil koddan ve parametreden istemcide üretilir.
- Anlaşılamayan istek 400, anlaşılıp anlamı reddedilen istek 422 ile bildirilir; ayrım
  kütükteki artışın nedenini de ayırır.
- Hata yolu istemcinin gönderdiği gövdedeki adrestir, sunucunun iç model adı değil; iç
  adlar bildirildiğinde eşleşme oranı dörtte sıfıra düşer.
- Alan adlandırma sözleşmesi toplam ve tek yönlü türetilebilir olmalıdır; hangi biçimin
  seçildiği değil, belgelenmiş ve her alan için tanımlı olması belirleyicidir.

## Sonraki Adım

Doğrulama şeması bu derste sabit kabul edildi: `/kalemler` en çok üç öğe alır, iade tarihi
en çok otuz gün ileri olabilir. Kütüphane bu sınırları değiştirmeye karar verdiğinde ne
olacak? Kalem sınırının beşe çıkması hiçbir istemciyi bozmaz; üçe düşmesi, dört kalemlik
istek yollayan her istemciyi bozar. Aynı gövde alanının adının değişmesi ise bütün
istemcileri aynı anda bozar. Sözleşmenin değişmesi kaçınılmazdır; sonraki ders bu
değişikliği yönetilebilir kılan aracı ele alır ve aynı kaynağın iki sürümünü tek sunucuda
yayımlayarak yol, başlık ve içerik tabanlı sürümlemenin istemci koduna maliyetini
karşılaştırır.
