Ana içeriğe geç

JSON Schema Validator: Ajv Kütüphanesi Kullanımı

JSON Schema Validator: Ajv Kütüphanesi Kullanımı - Veri ve API Rehberi

Sunucudan gelen JSON verisi her zaman beklenen yapıda gelmez. Bir alan eksik olabilir, tip yanlış olabilir ya da iç içe geçmiş bir nesne tamamen farklı bir biçimde ulaşabilir. Bu tutarsızlıkları yakalamak için kod genellikle uzun if/else zincirlerine dönüşür; her yeni alan için yeni bir kontrol eklenir. JSON Schema ve Ajv kütüphanesi bu zinciri ortadan kaldırır: veri yapısını bir kez tanımlarsınız, doğrulamayı Ajv yapar.

JSON Schema'nın doğrulama mantığı

JSON Schema, bir JSON belgesinin hangi yapıya sahip olması gerektiğini tanımlayan bir standarttır. Şema da JSON olarak yazılır; hedef verinin alan adlarını, tiplerini, zorunlu alanlarını ve kısıtlamalarını içerir. Bu sayede bir API yanıtını, kullanıcı formunu veya yapılandırma dosyasını tek bir şema tanımıyla denetlenebilir hale getirirsiniz.

Standart birkaç versiyona ayrılmıştır. Draft-04, uzun süre yayımlı API'lerde bulunur. Draft-07, if/then/else koşul bloklarını ve readOnly/writeOnly gibi meta anotasyonları ekledi. Draft-2019-09 ve Draft-2020-12 ise $vocabulary mekanizmasıyla şema uzantılarını resmileştirdi. Ajv, varsayılan olarak Draft-2020-12'yi destekler; eski şemalar için ajv-draft-04 veya ajv7 paketleri kullanılır. Projenin bağımlılıkları arasındaki diğer kütüphanelerin hangi draft beklediğini kontrol etmek, uyumsuzluk hatalarını baştan engeller.

Büyük hacimli veri akışlarında fark ölçülebilir olur. Sebebi şema derlemesidir: Joi veya Yup her çağrıda şemayı yeniden yorumlarken Ajv, onu bir kez derleyip optimize edilmiş bir JavaScript fonksiyonuna çevirir. Bu dönüşüm bir kez gerçekleşir; sonrasında yalnızca üretilen fonksiyon çalışır.

Ajv kurulumu ve ilk doğrulama akışı

Ajv, npm aracılığıyla kurulur:

npm install ajv

Temel kullanım üç adımdan oluşur: örneği oluştur, şemayı derle, veriyi doğrula.

import Ajv from "ajv";

const ajv = new Ajv();

const schema = {
  type: "object",
  properties: {
    id:    { type: "integer" },
    email: { type: "string", format: "email" },
    age:   { type: "integer", minimum: 0 }
  },
  required: ["id", "email"],
  additionalProperties: false
};

const validate = ajv.compile(schema);

const data = { id: 42, email: "ornek@domain.com", age: 28 };

if (validate(data)) {
  console.log("Geçerli");
} else {
  console.log(validate.errors);
}

Compile bir kez çalışır. Validate her istekte.

Derlenmiş validate fonksiyonunu modül düzeyinde saklayıp her istek döngüsünde yeniden çağırmak doğru yaklaşımdır; her istekte ajv.compile(schema) çağırmak ise hem gereksiz CPU tüketimi hem de bellek sızıntısı riskidir, çünkü her çağrı yeni bir fonksiyon nesnesi üretir ve Ajv'nin iç önbelleğini bypass eder.

additionalProperties: false önemli bir kısıttır. Şemada tanımlanmamış herhangi bir alan veri içinde bulunursa doğrulama başarısız olur. Güvenlik açısından değerli olmakla birlikte, ilerleyen sürümlerde API'ye yeni alan eklenmesi durumunda şemanın güncellenmesi gerekir. Bakım maliyeti buradan başlar.

Format doğrulaması (email, uri, date vb.) Ajv v8 itibarıyla varsayılan olarak devre dışıdır. Etkinleştirmek için ajv-formats paketini eklemek gerekir:

import addFormats from "ajv-formats";
const ajv = new Ajv();
addFormats(ajv);

Bu adım atlanırsa format: "email" kuralı sessizce yok sayılır; hata üretmez, uyarı vermez. Şemada format kullanılıyorsa bu kurulum zorunludur.

Tip, format ve kısıt kuralları

JSON Schema'nın doğrulama gücü birkaç temel anahtar sözcükte toplanır. Her biri farklı soruya cevap verir.

type alanı tek bir tip veya tip dizisi alır. "type": ["string", "null"] şeklinde yazılabilir; bu, alanın hem dolu hem de boş gelebileceğini ifade eder. REST API yanıtlarında opsiyonel alanlar çoğunlukla bu yapıyla modellenir.

minimum, maximum, minLength, maxLength ve pattern kısıtları veri sınırlarını belirler. pattern, düzenli ifade alır:

{
  "type": "string",
  "pattern": "^[A-Z]{2}-[0-9]{4}$"
}

Bu şema yalnızca TR-2024 gibi ülke kodu formatını kabul eder. Payload boyutunu kontrol altına almak için string alanlarda maxLength tanımlamak iyi bir pratiktir; sınırsız uzunlukta gelen string, downstream işlemlerde memory baskısı yaratabilir.

$ref ile ortak şema parçaları yeniden kullanılabilir. Büyük projelerde her endpoint için ayrı şema yazmak yerine, tekrar eden nesneler (adres, para birimi, kullanıcı kimliği) bağımsız tanımlar olarak $defs altında tutulur:

const schema = {
  $defs: {
    address: {
      type: "object",
      properties: {
        street: { type: "string" },
        city:   { type: "string" }
      },
      required: ["street", "city"]
    }
  },
  type: "object",
  properties: {
    billing:  { $ref: "#/$defs/address" },
    shipping: { $ref: "#/$defs/address" }
  }
};

Şema büyüdükçe bu yapı bakım maliyetini düşürür; aynı nesne tanımı değiştiğinde tek yerden güncellenir.

Birden fazla şemayı birleştirmek için allOf, anyOf ve oneOf kullanılır. anyOf esnek doğrulama için idealdir; veri, listelenen şemalardan en az birine uyuyorsa geçer. oneOf ise tam olarak bir şemaya uyumu zorunlu kılar ve hata mesajları daha ayrıntılı olur. API yanıtlarının birden fazla yapıda gelebileceği discriminated union senaryolarında bu ayrım önemlidir.

Hata nesnesi nasıl okunur?

Doğrulama başarısız olduğunda validate.errors, her hata için bir nesne içeren bir dizi döndürür. Bu dizi null değil, boş dizi değil; her hatanın ayrı bir nesne olarak listelendiği yapıdır.

Her hata nesnesinde birkaç alan vardır:

{
  instancePath: "/age",
  schemaPath: "#/properties/age/minimum",
  keyword: "minimum",
  params: { limit: 0 },
  message: "must be >= 0"
}

instancePath, hatanın veri içindeki konumunu JSON Pointer formatında gösterir. /age kök nesnenin age alanını, /items/2/email bir dizinin 3. elemanının email alanını ifade eder.

instancePath boş string döner. Bu hata kök nesnededir.

Örneğin required ihlali veya additionalProperties ihlali instancePath: "" ile gelir, çünkü hata alanın değerinde değil nesnenin kendisindedir. Bu ayrımı fark etmeden hata mesajlarını son kullanıcıya iletmek, anlamsız veya yanıltıcı çıktı üretir.

message alanı İngilizce gelir. Türkçe hata mesajları için ajv-i18n veya özel localize fonksiyonları kullanılır. Alternatif olarak keyword değerine bakarak kendi mesaj haritanızı oluşturabilirsiniz:

const messages = {
  required:              "Bu alan zorunludur.",
  minLength:             "Çok kısa.",
  minimum:               "Geçersiz değer.",
  additionalProperties:  "Beklenmeyen alan."
};

const humanErrors = validate.errors.map(err => ({
  field: err.instancePath.slice(1) || err.params?.missingProperty,
  message: messages[err.keyword] ?? "Geçersiz."
}));

Bu yaklaşım, Ajv'nin iç mesaj formatına bağımlılığı keser; kütüphane güncellendiğinde hata metinleri değişse bile kullanıcıya gösterilen çıktı sabit kalır.

Ajv'yi üretime taşırken atlanan noktalar

Ajv'nin doğru kurulduğu ama yanlış kullanıldığı senaryo şudur: her HTTP isteği geldiğinde şema nesnesi yeniden tanımlanır ve ajv.compile() çağrılır. Derlenmiş fonksiyon saklanmaz. Küçük projelerde bu fark edilmeyebilir; yüksek trafikte CPU profili bunu gösterir.

Çözüm, şema derlemesini uygulama başlangıcına taşımaktır:

// validators.js - modül yüklenirken bir kez çalışır
import Ajv from "ajv";
import addFormats from "ajv-formats";

const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

export const validateUser = ajv.compile(userSchema);
export const validateProduct = ajv.compile(productSchema);

allErrors: true seçeneği tüm hataları toplar; varsayılan davranış ilk hatada durur. Kullanıcıya tüm form hatalarını bir arada göstermek istediğinde bu seçenek gereklidir.

Asenkron doğrulama, şemada $async: true ile tanımlanır ve veritabanı sorgusu veya dış API çağrısı gerektiren kural senaryoları için kullanılır. Örneğin kullanıcı adının benzersizliği şema içinde tanımlanabilir; ancak bu her doğrulama çağrısının bir I/O işlemi içerdiği anlamına gelir. Basit alan doğrulamaları için bu maliyeti üstlenmek gereksizdir; asenkron kurallar yalnızca gerçekten dış kaynak gerektirdiğinde devreye sokulmalıdır.

Şema versiyonlama, büyük ekiplerde gözden kaçan bir bakım sorununa dönüşür. Sessizce kayar. API değiştikçe şema da değişir; ama şema değişikliğinin hangi sürümde, hangi alanı etkilediği takip edilmezse eski validate fonksiyonlarıyla yeni veri doğrulanmaya devam eder ve sistem hiçbir hata üretmez. Şema dosyalarını API versiyonlarıyla eşleştirmek ve değişiklik geçmişini açık tutmak, bu problemi başlamadan önce keser.

Bazı API'lerde binary veri (görsel, belge, imza hash'i) JSON alanı içinde Base64 ile taşınır. Ajv bu alanları contentEncoding: "base64" anotasyonuyla şema içinde tanımlar; ancak bu kuralı varsayılan olarak doğrulamaz, yalnızca metadata olarak kaydeder. Gerçek decode işlemi şema dışında kalır: Base64 ile kodlanmış veriyi çözümlemek için ayrı bir araç gerekir. Bu ayrım, şemanın ne yaptığını ve neyi dışarıda bıraktığını anlamak açısından önemlidir.

Production ortamında API yanıtlarını minify edilmiş biçimde iletmek, özellikle mobil bağlantılarda belirgin bir etki yaratır; girintili formatlama ve beyaz boşluklar yalnızca geliştirme aşamasında okuma kolaylığı sağlar. JSON payload boyutunu küçültmek için minify araçları kullanılabilir. Buna karşılık şema yazma aşamasında verinin gerçek yapısını görmek kritiktir; JSON Formatter ile veriyi görsel olarak incelemek hangi alanın hangi tipte geldiğini net biçimde ortaya koyar ve şema tanımlamasını tahmine değil gözleme dayalı yapar.