Structured Outputs · 19-c03
26 Haziran 2026 · Uygulama sınırı okuması
Bir LLM'in ürettiği JSON, prompt'ta ne kadar net şema tarif etmiş olursanız olun, bir validator onu kabul edene kadar güvenilmez girdidir. Doğrulama, prompt'un içine yazılan bir ricaya değil, uygulamanın sınırına ait bir bileşendir.
- Soru
- çıktıya güvenilir mi?
- Sınır
- model → validator → kod
- Kaldıraç
- schema as code
- Tuzak
- JSON geçerli = doğru sanmak
Bu yazıdaki kavramlar
- JSON Schema
- type · enum · required
- nested schema
- Pydantic · Zod
- Instructor
- Guardrails · OnFail
- partial streaming
- SLOT
Önceki yazılarda prompt'u kurduk ve modelden yapılandırılmış çıktı istedik. Şimdiki soru daha sert: model bize bir JSON döndürdü, peki bu JSON'a gerçekten güvenebilir miyiz? Cevap, çıktıyı nerede karşıladığınıza bağlı. "Geçerli JSON mu?" ile "benim şemama uyuyor mu?" ile "eksik alanlar gerçekten var mı?" çok farklı şeyleri ölçer.
Bu yazıda doğrulamayı bir uygulama sınırı (application boundary) olarak ele alıyorum: modelden gelen her şey, bir validator onu kabul edene kadar dış dünyadan gelen ham girdi gibi muamele görür. type, enum, required, nested schema ve streaming sırasındaki partial output'u tek tek kapılara ayırıyorum; her şemayı gerçek bir kaynağa bağlıyorum: JSON Schema 2020-12, OpenAI ve Azure structured outputs, Pydantic, Zod, Instructor, Guardrails AI ve SLOT. Diyagramlar kendi başına sürekli oynayıp her kapının neyi yakaladığını gösteriyor.
Çıktı, kabul edilene kadar güvenilmez girdidir
Bir validation pipeline'ının tek bir temel ilkesi vardır: modelin ürettiği metin, sizin tip sisteminize girene kadar dışarıdan gelen ham girdidir. Tıpkı bir kullanıcının form alanına yazdığı şey gibi. Pydantic bu ayrımı net koyar: validation, çıktı nesnesinin alanlarınıza ve kısıtlarınıza uyduğunu garanti eder, ham girdinin baştan doğru tipte olduğunu değil.
Bu yüzden doğrulamayı prompt'un içine "lütfen geçerli JSON döndür" diye yazmak yetmez. O bir ricadır, bir garanti değil. Garantiyi, çıktıyı karşılayan kod katmanı verir: parse et, şemaya göre kontrol et, geçerse tipli bir nesne üret, geçmezse reddet. Aşağıda modelin çıktısı kesik çizgili sınırı geçiyor; sınırın solunda "untrusted" bölgesinde, sağında ancak validator onayladıktan sonra "typed" bölgesinde.
Bu çerçeve sıkıcı görünebilir ama tüm yazının özü budur. Sınırı kabul ederseniz, geri kalan her şey (type check, enum, nested schema, retry) o sınırın üstünde duran somut kapılara dönüşür.
ham girdi bölgesi
tipli / doğrulanmış bölge
sınırı geçen tek şey, validator'ın kabul ettiği çıktıdır
Geçerli JSON, doğru JSON değildir
İlk kafa karışıklığı burada: "JSON parse edildi" ile "şemaya uyuyor" aynı şey değildir. Bir metin sözdizimsel olarak geçerli JSON olabilir ama beklediğiniz alanları, tipleri ya da değerleri taşımayabilir. OpenAI bu ayrımı kendi dokümanında net yapar: eski JSON mode geçerli JSON üretmeyi garanti eder, structured outputs ise verdiğiniz şemaya uymayı.
SLOT çalışması (EMNLP 2025) bunu ölçer hale getirir: bir cevabı ancak hem geçerli JSON ise hem de hedef şemanın anahtar adlarıyla ve değer tipleriyle birebir eşleşiyorsa "şema doğru" sayar. Üstelik şema doğruluğunu içerik benzerliğinden ayrı ölçer, çünkü bir çıktı sözdizimini geçip yine de anlamı kaybedebilir ya da bozabilir.
Aşağıda aynı metin iki kapıdan geçiyor. İlki sadece "parse oldu mu?" diye sorar ve neredeyse her şeye yeşil yakar. İkincisi "alan adları ve tipler şemaya uyuyor mu?" diye sorar; eksik alan ya da yanlış tip onu kırmızıya çevirir.
parse oldu mu?
şemaya uyuyor mu?
tip ve enum uyuşmuyor
geçerli JSON gerekli ama yeterli değildir
Schema as code: Pydantic ve Zod
Doğrulamayı sınıra koymanın en pratik yolu, şemayı kod olarak yazmaktır. Python tarafında Pydantic, TypeScript tarafında Zod bunu yapar: alanları, tipleri ve kısıtları kodda tanımlarsınız, kütüphane de runtime'da gelen veriyi bu tanıma göre doğrular.
İkisi de aynı sınır mantığını taşır. Pydantic'te model_validate_json başarılıysa tipli bir model örneği döner, başarısızsa tek bir ValidationError içinde bulduğu tüm hataları toplar. Zod'da parse başarıda derin kopyalı tipli bir değer döner, hatada ZodError fırlatır; safeParse ise fırlatmak yerine ya veriyi ya hatayı taşıyan bir sonuç nesnesi verir. Bu yüzden bunlar pipeline'ın "son parser"ı olmalıdır: tipli değeri ya da yapılandırılmış hatayı üreten yer.
Aşağıda aynı ham metin iki kütüphaneye giriyor. Başarı dalında sınırı geçip tipli bir nesne oluyor; hata dalında bir yapılandırılmış hata nesnesi olarak geri dönüyor. İki kütüphane farklı isimler kullanıyor ama sınır davranışı aynı.
Pydantic
ham JSON metni
Zod
ham JSON metni
Üç temel kapı: type, enum, required
JSON Schema 2020-12, doğrulamayı bir JSON örneği üzerindeki assertion'lar (iddialar) olarak tanımlar: tüm uygulanabilir konumlar iddia edilen kısıtları sağlıyorsa örnek geçerlidir. Bu soyut görünür ama günlük hayatta üç kapıya iner. type, değerin beklenen ilkel tipte (string, number, integer, boolean, object, array, null) olmasını ister. enum, değerin izin verilen sabit listeden biriyle eşit olmasını ister. required ise yalnızca object'lere uygulanır ve listelenen her alan adının var olmasını ister.
Önemli bir incelik: required atlanırsa boş liste gibi davranır, yani "alan zorunlu değil" demektir. Şema açıkça söylemedikçe uygulamanız bir alanı zorunlu varsaymamalıdır. Ayrıca format anahtarı varsayılan olarak bir assertion değil, bir annotation'dır; e-posta ya da tarih gibi format'ları gerçekten zorlamak istiyorsanız bunu uygulama katmanında ayrıca doğrulamanız gerekir.
Aşağıda bir aday nesne soldan girip üç kapıdan sırayla geçiyor: type, enum, required. Üçünü de geçen nesne sağda yeşille kabul ediliyor. Sonraki turda enum kapısında takılan bir nesne tam o kapıda kırmızı yanıp reddediliyor; hangi kuralın kırdığı bu sayede görünür oluyor.
Nested schema: doğrulama bir ağaçtır
Gerçek çıktılar düz değildir: bir sipariş nesnesi içinde bir müşteri, müşterinin içinde bir adres, siparişin içinde bir kalem listesi vardır. JSON Schema bunu uygulayıcı (applicator) anahtarlarıyla çözer: properties, items, $ref gibi anahtarlar, kuralları JSON örneğinin iç konumlarına uygular. Yani doğrulama bir tek kutu değil, bir ağaç üzerinde yürür.
Pydantic'te modeller başka modelleri ve model listelerini içerebilir; hata oluştuğunda mesaj nested.inner gibi bir yola işaret eder, böylece hangi derinlikte kırıldığını bilirsiniz. Zod da nested object, array, recursive object ve discriminated union destekler. Burada bir politika kararı vardır: bilinmeyen anahtarlar ne olacak? Pydantic varsayılan olarak yok sayar (forbid ile reddedebilir, allow ile saklayabilir); Zod varsayılan olarak siler, strict object modunda ise bilinmeyen anahtarda hata fırlatır. Bu "sil ya da reddet" kararı veri kaybı davranışını doğrudan belirler.
Aşağıda nested bir nesne dış kabuktan içeri doğru kapı kapı doğrulanıyor: order, içindeki customer, onun içindeki address. Doğrulama içeri yürürken yeşil ilerliyor; address derinliğinde geçersiz bir alan çıkınca tam o düğüm kırmızı yanıyor ve hata yolu o derinliği işaret ediyor.
hata yolu · customer.address.zip
Provider'ın şeması sizi app-side doğrulamadan kurtarmaz
OpenAI ve Azure gibi sağlayıcılar artık şema uyumunu üretim sırasında zorlayabiliyor (strict mode). Bu güçlü, ama tehlikeli bir rahatlama yaratır: "provider zaten zorluyorsa ben neden doğrulayayım?" Cevap, provider subset'inin sizin tam şemanızdan dar olmasıdır.
Azure dokümanı somut kurallar koyar: structured outputs'ta tüm alanlar required listesinde olmalı; opsiyonelliği eksik alanla değil, tipe null ekleyerek (örneğin string ya da null) ifade edersiniz. Object'ler additionalProperties: false kullanmalı; toplam en fazla 100 property ve beş seviye nesting desteklenir. Üstelik bazı tipe özel JSON Schema anahtarları (min/max uzunluk, bazı pattern ve array kısıtları) provider decoding'inin dışında kalır, yani onları yine runtime validator'ınız zorlamalıdır. OpenAI de refusal ve yarım kalmış çıktı gibi başarısızlık durumlarının programatik olarak ele alınması gerektiğini söyler: körlemesine parse etmeyin.
Aşağıda iki katman üst üste. Üstte provider'ın garanti ettiği dar bölge (strict şema, required-nullable, additionalProperties: false). Altta sizin app-side validator'ınızın kapattığı boşluklar (uzunluk, pattern, refusal, yarım çıktı). İkinci katman olmadan, birinci katmanın bıraktığı boşluklar açıkta kalır.
provider garantisi (strict)
app-side validator
provider zorlaması, app-side doğrulamanın yerini almaz
Partial output: streaming sırasında doğrulama gevşer
Çıktıyı streaming ile, token token alırsanız, nesne anlık olarak yarımdır: bazı alanlar daha gelmemiştir. Burada required kuralını olduğu gibi uygularsanız her ara kare "eksik alan" diye reddedilir. Instructor bu modu açıkça ayırır: bir modelin partial sürümünü üretir, bu sürümde tüm alanlar opsiyoneldir ve çıkarım tamamlanana kadar artımlı (incremental) anlık görüntüler verir.
Bunun bir bedeli var: Instructor dokümanı, streaming sırasında validator'ların çalışmadığını söyler, çünkü doğrulama eksik bir duruma güvenilir biçimde uygulanamaz. Yani partial mod ile required/validated mod farklı operasyonel modlardır. Pratik kalıp şudur: stream sırasında alanlar dolarken yumuşak gösterirsiniz, ama tam nesne tamamlandığında required'ı ve tüm validator'ları son bir kez asıl şemaya göre çalıştırırsınız.
Aşağıda bir nesne streaming ile alan alan doluyor. Henüz gelmemiş alanlar gri ve opsiyonel; her alan geldikçe kendi mini-tipini doğrulayıp yeşile dönüyor. Stream bitince altta tek bir "final required check" kapısı yanıp tüm nesneyi asıl şemaya göre son kez doğruluyor.
Hata gelince: retry, fix, filter, reject
Bir validator reddettiğinde iş bitmez, asıl iş başlar: ne yapacaksınız? Instructor pipeline-biçimli bir cevap verir: modeli tanımla, isteği gönder, cevabı doğrula, geçerliyse nesneyi döndür; değilse hata bağlamını LLM'e geri gönder ve max retry'a kadar tekrar dene, sonra ValidationError'ı fırlat. Yani doğrulama hatası, modele verilen bir sonraki ipucudur.
Guardrails AI bu kararları açık politikalara çevirir. Guard, LLM çağrısını saran ana arayüzdür: doğrulamayı yönetir, çağrı geçmişini tutar ve ham çıktı, doğrulanmış çıktı, geçti mi bilgisini birlikte verir. Validator'lar PassResult ya da FailResult döner; başarısızlıkta yapılandırılmış OnFail eylemleri devreye girer: REASK (modele tekrar sor), FIX (otomatik düzelt), FILTER (yalnızca kıran alanı ele, gerisini tut), REFRAIN (çıktıyı tümden geri çek), NOOP (işaretle ama geçir), EXCEPTION (hata fırlat). Bu, geçer/geçmez tek bitten çok daha zengin bir kurtarma yelpazesidir ve hepsi gözlemlenebilirdir.
Aşağıda reddedilen bir çıktı bir politika anahtarına giriyor ve farklı OnFail dallarına ayrılıyor: REASK modele geri döner, FIX düzeltilmiş haliyle geçer, FILTER kıran alanı atıp gerisini geçirir, REFRAIN çıktıyı tümden durdurur. Her tur farklı bir dalı vurgulayarak hangi politikanın ne yaptığını gösterir.
kurtarma politikaları açık ve gözlemlenebilir olmalıdır
Doğrulama hattı: katman katman
Tek bir kontrol bir çıktıyı güvenli kılmaz. Sağlam bir doğrulama hattı katman katmandır: önce sınırın kabulü (model çıktısı = untrusted girdi), sonra parse (geçerli JSON mu), sonra şema doğruluğu (type, enum, required), nested ağacın derinliği, provider subset'inin bıraktığı app-side boşluklar, streaming için partial/final ayrımı ve en sonda hata gelince devreye giren OnFail politikaları.
Bu katmanları birlikte okumak, "modelin çıktısına güveniyor muyum?" sorusunu "hangi kapıda, hangi kuralla, hata olursa hangi politikayla doğruluyorum?" sorusuna çevirir. İyi bir validation pipeline, modelin ne ürettiğini değil, sisteminizin sınırının nerede tuttuğunu söyler.
Modelin çıktısı, bir validator onu kabul edene kadar güvenilmez girdidir. Bütün hat bu tek cümleyi ciddiye almaktan ibarettir.
Bu yüzden "çıktıya güveniyor muyum?" demeden önce sorun: hangi kapı parse'ı, hangi kapı şemayı, hangi katman provider boşluğunu kapatıyor ve hata olursa hangi OnFail politikası devreye giriyor?
Kaynaklar
- S1Wright, Andrews, Hutton, Dennis · JSON Schema 2020-12 Core & Validation
- S2OpenAI · Structured Outputs (API docs)
- S3Microsoft · Azure OpenAI Structured Outputs (Learn)
- S4Pydantic · Models & JSON Schema docs
- S5Zod · Basics & API docs
- S6Instructor · Validation & Partial Streaming docs
- S7Guardrails AI · Guard, Validators & OnFail Actions docs
- S8Shen et al., SLOT: Structuring the Output of LLMs, EMNLP 2025 Industry
