Serbest metin çıktısı üretimde güvenilmezdir. Model JSON’u bir kod bloğuna sarabilir, fazladan açıklama ekleyebilir, bazen zorunlu bir alanı atlayabilir. Bu belirsizliği try/except json.loads(...) zinciriyle çözmeye çalışmak hem zaman çalar hem bakımı zorlaştırır.
Structured outputs bu sorunu modelin kendisine devreder: token seçimini doğrudan şemaya kilitler, böylece çıktı her zaman parse edilebilir kalır. Bu rehberde nasıl çalıştığını, hangi araçların ne sunduğunu ve üretim ortamında nelere dikkat edeceğini adım adım göreceksin.
Structured outputs neden gerekli?
Şöyle bir senaryo düşün: kullanıcıdan gelen bir e-postadan ürün adı, fiyatı ve kategorisini çıkarmak istiyorsun. Modele şunu soruyorsun:
“Şu metinden ürün adını, fiyatını ve kategorisini JSON olarak ver.”
Model ilk denemede şunu döndürebilir:
{"ad": "Kablosuz Klavye", "fiyat": "349 TL", "kategori": "Bilgisayar"}
İkinci denemede ise:
Tabii, işte ürün bilgileri:
```json
{"name": "Wireless Keyboard", "price": 349, "category": "Electronics"}
Üçüncüde ise hiç JSON olmadan düz metin. Alan adları farklı, dil farklı, format farklı. Bu tutarsızlık CI/CD pipeline'larında, otomatik veri işleme akışlarında ve [AI agent](/blog/sifirdan-ai-agent-yapimi/) mimarilerinde ciddi sorunlara yol açar. Regex yazmak, birden fazla parsing denemesi eklemek, alan adlarını normalize etmek için ayrıca kod geliştirmek gerekir.
Parse katmanı büyüdükçe bakım yükü de artar. Model güncellendiğinde çıktı formatı kayar ve eski parsing kodu sessizce yanlış veri üretmeye başlar. Üretim ortamında bu tür hatalar gece alarmına dönüşür.
Structured outputs bu döngüyü keser. Model şemaya kilitlendiğinde alanlar her zaman aynı isimle, aynı tiple gelir ve fazladan metin eklenmez.
## JSON mode ile structured outputs: iki ayrı kavram
Bu iki terim çoğu zaman birbirinin yerine kullanılır, ama aralarında önemli bir fark var.
**JSON Mode** (`response_format: { type: "json_object" }`), modelin geçerli bir JSON nesnesi üretmesini zorunlu kılar. Alan adlarını, tiplerini veya hangi alanların zorunlu olduğunu garanti etmez. Model yine de istediği yapıyı oluşturabilir; yalnızca söz dizimi geçerliliği sağlanmış olur.
**Structured Outputs** ise `json_schema` parametresiyle birlikte `"strict": true` gönderildiğinde devreye girer. Model artık verdiğin şemayı birebir takip etmek zorundadır: alan adları sabit, tipler sabit, zorunlu alanlar her zaman mevcut.
Pratikte farkı şöyle düşün: JSON Mode bir trafik kuralı gibi davranır, "geçerli bir yol üzerinde git" der ama nereye gideceğini söylemez. Structured Outputs ise GPS gibi davranır, tam rotayı önceden belirler. Düşük riskli bir prototipte JSON Mode yeterli olabilir; pipeline otomasyonunda veya başka bir servisin tükettiği bir çıktıda alan adlarının ve tiplerin garanti edilmesi gerekir.
```python
# JSON Mode — geçerli JSON ama alan garantisi yok
response = client.chat.completions.create(
model="gpt-4o",
response_format={"type": "json_object"},
messages=[{"role": "user", "content": "Ürün bilgisini JSON ver."}]
)
# Structured Outputs: şemaya tam bağlılık
response = client.chat.completions.create(
model="gpt-4o",
response_format={
"type": "json_schema",
"json_schema": {
"name": "urun_bilgisi",
"strict": True,
"schema": {
"type": "object",
"properties": {
"ad": {"type": "string"},
"fiyat": {"type": "number"},
"kategori": {"type": "string"}
},
"required": ["ad", "fiyat", "kategori"],
"additionalProperties": False
}
}
},
messages=[{"role": "user", "content": "Ürün bilgisini JSON ver."}]
)
"strict": true olmadan json_schema tipi belirtsen bile model zorunlulukları daha gevşek yorumlayabilir. Bu parametreyi üretim ortamında her zaman açık bırakman gerekir.
OpenAI API ile native structured outputs
OpenAI’nin Python SDK’sı .parse() metoduyla Pydantic modelini doğrudan entegre eder. JSON string’i elle parse etmek yerine model yanıtı otomatik olarak Python nesnesine dönüşür.

from pydantic import BaseModel
from openai import OpenAI
client = OpenAI()
class UrunBilgisi(BaseModel):
ad: str
fiyat: float
kategori: str
stokta_var: bool
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "Kullanıcının metninden ürün bilgisini çıkar."},
{"role": "user", "content": "349 TL'lik kablosuz klavye stokta mevcut, Aksesuar kategorisinde."}
],
response_format=UrunBilgisi,
)
urun = completion.choices[0].message.parsed
print(urun.ad) # "kablosuz klavye"
print(urun.fiyat) # 349.0
print(urun.stokta_var) # True
completion.choices[0].message.parsed doğrudan UrunBilgisi örneği döndürür. json.loads() çağrısı, alan kontrolü veya tip dönüşümü gerekmez.
Bu yaklaşım özellikle karmaşık veri çıkarma görevlerinde fark yaratır. Modelin şemayı öğrenmesi için örnekler vermene de gerek kalmaz; Pydantic modelin tanımı şemayı otomatik oluşturur.
Pydantic ile şema tanımlamak
Pydantic, Python’un type hint ekosistemiyle tam uyumlu bir veri doğrulama kütüphanesidir. Structured outputs için şema tanımlamak üzere endüstri standardı haline geldi; IDE desteği ve validation mantığı aynı yerde toplanıyor.
Pydantic’in bu kadar yaygınlaşmasının birkaç pratik nedeni var. Birincisi, BaseModel alt sınıfları otomatik olarak JSON Schema’ya dönüştürülür; OpenAI, Anthropic ve Gemini bu şemayı doğrudan kabul eder. İkincisi, alan türleri Python’un kendi int, str, float, datetime gibi yapıları olduğundan öğrenme eğrisi neredeyse sıfırdır. Üçüncüsü, VS Code ve PyCharm gibi editörler modelin alanlarını otomatik tamamlar ve tip hatalarını anında gösterir; bu da runtime’da değil geliştirme aşamasında sorunları yakalar.
Basit bir model şöyle görünür:
from pydantic import BaseModel, Field
from typing import Optional, List
class Randevu(BaseModel):
baslik: str = Field(description="Randevunun kısa başlığı")
tarih: str = Field(description="ISO 8601 formatında, örn. 2026-06-15")
saat: str = Field(description="HH:MM formatında")
yer: Optional[str] = Field(default=None, description="Fiziksel konum veya online link")
katilimcilar: List[str] = Field(default_factory=list, description="E-posta adresleri")
Field(description=...) parametresi iki işe yarar: hem geliştirici için dokümantasyon oluşturur hem de modele şemada ipucu olarak gönderilir. Model bu açıklamaları okuyarak alanı daha doğru doldurmaya çalışır.
İç içe modeller için:
from typing import Union, Literal
class BasitGelir(BaseModel):
tip: Literal["maas", "freelance"]
tutar: float
class KarmasikGelir(BaseModel):
tip: Literal["kira", "temettü"]
tutar: float
vergi_orani: float
class FinansalOzet(BaseModel):
ad: str
gelirler: List[Union[BasitGelir, KarmasikGelir]]
toplam_varlik: float
Union ve Literal tipleri şemayı daha spesifik kılar. Model hangi tip değer beklediğini net görür ve halüsinasyon riski azalır; ancak Union kullanımı bazı model sağlayıcılarında şema karmaşıklığını artırdığından dikkatli olmak gerekir.
Instructor kütüphanesi: tek satırda yapılandırılmış çıktı
Instructor, pip install instructor ile kurulur ve birden fazla sağlayıcıyı tek bir API’nin arkasında soyutlar. Retry mantığı ve validation hatası yönetimi dahili olarak ele alınır.

import instructor
from openai import OpenAI
from pydantic import BaseModel
client = instructor.from_openai(OpenAI())
class UrunBilgisi(BaseModel):
ad: str
fiyat: float
kategori: str
urun = client.chat.completions.create(
model="gpt-4o",
response_model=UrunBilgisi,
messages=[{"role": "user", "content": "349 TL kablosuz klavye, Aksesuar."}]
)
print(urun.ad) # "kablosuz klavye"
print(urun.fiyat) # 349.0
Validation hatası oluştuğunda Instructor varsayılan olarak yeniden dener. max_retries parametresiyle bu sayıyı kontrol edebilirsin:
urun = client.chat.completions.create(
model="gpt-4o",
response_model=UrunBilgisi,
max_retries=3,
messages=[...]
)
Instructor aynı zamanda Claude ve Gemini destekler:
# Anthropic Claude
import anthropic
claude_client = instructor.from_anthropic(anthropic.Anthropic())
# Google Gemini
import google.generativeai as genai
gemini_client = instructor.from_google(genai)
Tek bir Pydantic modeliyle üç farklı sağlayıcı üzerinde çalışabilmek, sağlayıcı geçişlerini ciddi ölçüde kolaylaştırır.
Anthropic ve Google’ın yaklaşımı
Her sağlayıcı structured outputs için farklı bir API yüzeyi sunar.
Anthropic Claude için betas parametresine "output-128k-2025-02-19" eklemek ve response_format ile şemayı geçirmek gerekir:
import anthropic
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-opus-4-7-20251101",
betas=["output-128k-2025-02-19"],
max_tokens=1024,
response_format={
"type": "json_schema",
"json_schema": {
"name": "urun_bilgisi",
"schema": {
"type": "object",
"properties": {
"ad": {"type": "string"},
"fiyat": {"type": "number"}
},
"required": ["ad", "fiyat"]
}
}
},
messages=[{"role": "user", "content": "349 TL kablosuz klavye."}]
)
Google Gemini için generation_config içine response_mime_type ve response_schema eklenir:
import google.generativeai as genai
from google.ai.generativelanguage_v1beta.types import content
model = genai.GenerativeModel("gemini-1.5-pro")
response = model.generate_content(
"349 TL kablosuz klavye, Aksesuar kategorisi.",
generation_config=genai.GenerationConfig(
response_mime_type="application/json",
response_schema=content.Schema(
type=content.Type.OBJECT,
properties={
"ad": content.Schema(type=content.Type.STRING),
"fiyat": content.Schema(type=content.Type.NUMBER),
},
),
),
)
Üç sağlayıcının da ayrı syntax’ı var. Çapraz sağlayıcı bir kod tabanı yönetiyorsan Instructor bu farklılıkları soyutlar ve bakım yükünü azaltır.
Hangi sağlayıcıyı seçeceğine karar verirken dikkat etmen gereken birkaç nokta var. OpenAI’nin gpt-4o ailesinde strict mode en olgun haldedir; belgeleri en kapsamlı, topluluk desteği en geniş. Anthropic’in Claude’u özellikle uzun bağlam gerektiren görevlerde (binlerce kelimelik doküman parçalarını şemaya dökmek gibi) tercih edilir; 128k çıktı beta’sıyla büyük şemalar için daha geniş bir pencere sunar. Google Gemini ise özellikle Google Cloud ekosistemindeysen veya Vertex AI altyapısını kullanıyorsan doğal bir seçim olur.
Üretim ortamında sağlayıcı değiştirmek istersem ne olur? Instructor gibi bir soyutlama katmanı kullanıyorsan Pydantic modelini değiştirmene gerek kalmaz; yalnızca instructor.from_openai(...) yerine instructor.from_anthropic(...) yazarsın. Native SDK’larla çalışıyorsan şemayı farklı parametrelerle tekrar geçirmen gerekir. Bu nedenle birden fazla sağlayıcı deneyeceksen Instructor başlangıçtan itibaren daha az tekrar sağlar.
İç içe modeller ve karmaşık şemalar
Gerçek dünya görevleri genellikle tek düz obje değil, iç içe yapılar gerektirir. E-posta içinden toplantı bilgisi çıkarmak, bir PDF’ten ürün listesi almak, uzun bir metin üzerinde bölüm bölüm analiz yapmak bunların hepsinde bir iç içe model kurgulamak gerekir. Şema ne kadar iyi modellerse modelin çıktısı o kadar güvenilir olur.
İyi bir kural: şemayı önce kağıda çiz. Neyin zorunlu, neyin isteğe bağlı, neyin tekrarlanabilir olduğuna karar ver. Modelin her koşulda doldurabileceği alanları required listesine koy; nadiren var olan bilgileri Optional yap. Çok sayıda Optional alan varsa şemanın fazla geniş kapsamlı olduğuna işaret eder ve modelin hangi alanları doldurmak istediğini tahmin etmek zorunda kalmasına yol açar.
Bir iş ilanı çıkarma örneği:
from pydantic import BaseModel, Field, model_validator
from typing import List, Optional
class Gereksinim(BaseModel):
yetkinlik: str
zorunlu: bool
class Maas(BaseModel):
min_tutar: Optional[float] = None
max_tutar: Optional[float] = None
para_birimi: str = "TRY"
@model_validator(mode="after")
def aralik_kontrolu(self):
if self.min_tutar and self.max_tutar:
if self.min_tutar > self.max_tutar:
raise ValueError("Min maaş max'dan büyük olamaz")
return self
class IsIlani(BaseModel):
pozisyon: str
sirket: str
sehir: str
uzaktan: bool
gereksinimler: List[Gereksinim]
maas: Optional[Maas] = None
model_validator, şemayı şekillendirmez ama Instructor’ın retry döngüsüyle entegre çalışır. Validation hatası yükselirse Instructor model yanıtını hata mesajıyla birlikte tekrar gönderir ve modelin düzeltilmiş bir çıktı üretmesi beklenir.
OpenAI’nin strict modunda JSON Schema iç içe geçme derinliği 5 seviyeyle sınırlıdır. Daha derin yapılar gerekliyse şemayı düzleştirmek ya da ayrı API çağrılarına bölmek gerekir.
Ne zaman iç içe, ne zaman düz şema kullanmalısın? Düz şemalar daha az token harcar ve model daha az hata yapar. İç içe yapılar, verinin doğası gereği hiyerarşik olduğu durumlarda mantıklı; mesela bir faturada birden fazla kalem (her birinin adı, miktarı, fiyatı) olduğunda List[FaturaKalem] kullanmak doğaldır. Ama sadece “şema daha düzenli görünsün” diye iç içe model kurmak genellikle token maliyeti ve hata toleransı açısından dezavantaj oluşturur.
Üretim ortamında dikkat edilecekler
Token overhead: Şemayı her istek için sistem bağlamına eklemek token sayısını artırır. Büyük, karmaşık şemalarda bu maliyet fark edilir. Şemayı minimum tutmak, gereksiz description alanlarını kısaltmak ve "strict": true modunu kullanmak maliyet optimizasyonu için iyi başlangıç noktaları.
Retry stratejisi: Instructor otomatik retry yapar ama max_retries=0 ile bunu devre dışı bırakabilirsin. Kritik üretim yollarında retry sayısını sınırlı tutmak ve halüsinasyon riski taşıyan alanlar için ek doğrulama eklemek mantıklıdır.
from instructor.exceptions import InstructorRetryException
try:
sonuc = client.chat.completions.create(
model="gpt-4o",
response_model=UrunBilgisi,
max_retries=2,
messages=[...]
)
except InstructorRetryException as e:
# Fallback mantığı veya kullanıcı bildirimi
logger.error(f"Structured output başarısız: {e}")
Şema sürümleme: Pydantic modelini değiştirdiğinde modelin ürettiği çıktı da değişebilir. Şemayı versiyonlamak ve değişiklikleri açık biçimde izlemek, sessiz veri kaymasını önler. Bunu özellikle Optional alanı zorunlu hale getirdiğinde veya bir alan adını yeniden adlandırdığında hissedersin. Pratikte şema değişikliklerini semantic version gibi yönetmek iyi bir alışkanlık: küçük değişiklikler patch, yeni zorunlu alan eklenmesi minor, şema yapısını kökten bozmak major olarak etiketlenebilir. Bu yaklaşım hem API kullanıcılarına hem de gelecekteki sana ne değiştiğini hızlıca anlatır.
Caching: OpenAI’nin prompt caching’i şemayı da kapsar. Büyük şemalı sistemlerde istek başına maliyetin düştüğünü ölçmek için cache hit oranlarını izlemen gerekir. Prompt caching’i ayrıntılı ele alan prompt engineering rehberinde bağlam penceresi optimizasyonu konusu var.
Structured outputs ile function calling: ne zaman hangisi?
Bu ikisi farklı sorunları çözer. Function calling, modelin bir araç çağırmasını sağlar; structured outputs ise modelin ürettiği veriyi belirli bir forma kilitler. Kavramsal ayrım şu:
- Function calling: “Hava durumu API’sini çağır, sonucu bana getir.”
- Structured outputs: “Dönen yanıtı her zaman bu Pydantic modeliyle temsil et.”
İkisi birbirini dışlamaz. MCP (Model Context Protocol) tabanlı sistemlerde tool yanıtlarını Pydantic modeline parse etmek standart bir pattern:
class HavaDurumu(BaseModel):
sehir: str
sicaklik: float
durum: Literal["güneşli", "bulutlu", "yağmurlu", "karlı"]
nem_yuzdesi: int
# Tool çıktısını structured output olarak parse et
hava = HavaDurumu.model_validate_json(tool_result_json)
RAG (Retrieval-Augmented Generation) mimarilerinde de aynı pattern çalışır: belge parçacıklarından çıkarılan bilgiyi belirli bir şemaya dökebilirsin. Bu AI agent yapımı yazısında tartışılan çok adımlı akışlarda çıktı tutarlılığını artırmak için yaygın kullanılan bir teknik.
Araç çağrıyorsan function calling, modelden veri şekillendirmesini istiyorsan structured outputs. İkisi bir arada kullandığında function calling toolchain’i tetikler, structured outputs da o toolchain’in her adımındaki veri şekillerini sabitler. Vektör veritabanları üzerinden çalışan arama sistemlerinde bu kombinasyon arama sorgusunu ve dönen belge parçalarını normalize etmek için aynı anda kullanılabilir.
Eğer yeni başlıyorsan, en basit seçim OpenAI SDK’nın native .parse() yöntemiyle bir Pydantic modeli denelemektir. İlk denemede şemanın ne kadar küçük tutulabileceğini, hangi alanların gerçekten zorunlu olduğunu göreceksin. Oradan başlayarak gereksinim büyüdükçe Instructor’ı veya çapraz sağlayıcı desteğini eklemek çok daha kolay olur.
Kurulum basit, kullanım doğrudan: bir Pydantic modeli tanımla, modeli çağır, parse edilmiş Python nesnesini kullan. Retry ve validation mantığı için Instructor; çapraz sağlayıcı gereksinim olmaksızın sade bir Python projesinde ise OpenAI SDK’nın native .parse() yöntemi yeterli bir başlangıç noktası.