Kullanıcıların gönderdiği faturalardan yapılandırılmış veri çıkarmak için GPT-4'ü çağıran bir özellik yayına aldın. Development'ta mükemmel çalışıyor — model her seferinde temiz bir JSON objesi döndürüyor. Sonra production'da, sabah 2'de, bir Sentry alert'ü alıyorsun: JSON.parse: unexpected token. Model asıl payload'dan önce cevabını "Elbette! İşte istediğin JSON:" ile açmaya karar vermiş. Bir hafta sonra, aynı özellik, farklı bug: model total_amount yerine totalAmount döndürüyor ve aşağı akış database write'ın alanı sessizce düşürüyor. Eğer LLM çıktısının güvenilirliği etrafında prompt ile yol almaya çalışıyorsan, OpenAI Structured Outputs beklediğin çözümdür.

OpenAI tarafından Ağustos 2024'te yayınlanan Structured Outputs, response_format parametresi üzerinden bir JSON Schema vermeni ve bu şemayla tam olarak eşleşen geçerli olması garantili bir cevap almanı sağlar. Bu, sadece çıktının geçerli JSON olduğunu garanti eden — belirli bir şekil olduğunu değil — eski JSON modundan ({"type": "json_object"}) farklıdır. Ayrıca, modelin çıktısını bir tool çağrısına yönlendiren ama kendi tören katmanını ekleyen function calling'den de farklıdır. Structured Outputs en temiz yoldur: istediğin şekli tarif et, her seferinde tam olarak o şekli geri al. Arka planda OpenAI kısıtlı decoding kullanır — modelin token sampling'i senin şeman tarafından yönlendirilir, yani model geçersiz bir cevap üretemez.

İlk Structured Output'un

python
from openai import OpenAI
import json

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {
            "role": "user",
            "content": "Extract the vendor name, invoice number, and total amount from this text: "
                       "Invoice #INV-2024-0892 from Acme Supplies Ltd. Total due: $1,450.00"
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "invoice_extract",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "vendor_name":     {"type": "string"},
                    "invoice_number":  {"type": "string"},
                    "total_amount":    {"type": "number"}
                },
                "required": ["vendor_name", "invoice_number", "total_amount"],
                "additionalProperties": False
            }
        }
    }
)

data = json.loads(response.choices[0].message.content)
print(data)
# {"vendor_name": "Acme Supplies Ltd", "invoice_number": "INV-2024-0892", "total_amount": 1450.0}

Burada dikkat edilecek üç şey. Birincisi, model gpt-4o-2024-08-06 — Structured Outputs, onu açıkça destekleyen bir model gerektirir (GPT-4o için -2024-08-06 snapshot'ı veya sonrası ya da gpt-4o-mini). İkincisi, response_format.type, "json_object" değil "json_schema"'dır. Üçüncüsü, sana garantiyi veren "strict": True'dur — onsuz tekrar best-effort bölgesindesin. name alanı modelin gördüğü bir etikettir; parsing üzerinde etkisi yoktur ama API loglarını okunabilir yapar.

JSON Şemasını Tasarlamak

İşte bir ürün kataloğu çıkarma görevi için daha gerçekçi bir şema — yapılandırılmamış ürün açıklamalarından, e-ticaret ilanlarından veya PDF veri sayfalarından yapılandırılmış veri çekmek için kullanacağın türden. Şemanı API çağrılarına bağlamadan önce görsel olarak kurmak ve doğrulamak için JSON Schema Generator'ı kullan.

json
{
  "type": "object",
  "properties": {
    "product_name": {
      "type": "string",
      "description": "The full commercial name of the product"
    },
    "sku": {
      "type": "string",
      "description": "Stock keeping unit identifier"
    },
    "price_usd": {
      "type": "number",
      "description": "Price in US dollars, numeric only"
    },
    "in_stock": {
      "type": "boolean"
    },
    "categories": {
      "type": "array",
      "items": { "type": "string" }
    },
    "dimensions": {
      "type": "object",
      "properties": {
        "width_cm":  { "type": "number" },
        "height_cm": { "type": "number" },
        "depth_cm":  { "type": "number" }
      },
      "required": ["width_cm", "height_cm", "depth_cm"],
      "additionalProperties": false
    }
  },
  "required": [
    "product_name", "sku", "price_usd",
    "in_stock", "categories", "dimensions"
  ],
  "additionalProperties": false
}
  • Strict mode'da tüm property'ler required'da olmalıdır. Opsiyonel alanların olamaz. Bir alan kaynak veride bulunmayabilirse, union tipi kullan: {"type": ["string", "null"]} ve her zaman required'a dahil et.
  • additionalProperties her object seviyesinde false olmalıdır. Bu özyinelemeli olarak uygulanır — iç içe object'lerin de buna ihtiyacı var, sadece root değil.
  • Strict mode'da desteklenen tipler: string, number, integer, boolean, null, array, object. Tip union'ları (["string", "null"]) izinlidir.
  • Strict mode'da $ref veya özyinelemeli şema yok. Her şey inline olmalıdır. Yeniden kullanılabilir bir tanıma ihtiyacın varsa kopyala.
  • description alanlarını cömertçe ekle. Model onları okur. "Amerikan doları cinsinden fiyat, yalnızca sayısal — para birimi sembolü ekleme" demek, modelin doğru tahmin etmesini ummaktan daha temiz çıktı verir.
  • Enum'lar çalışır. {"type": "string", "enum": ["pending", "shipped", "delivered"]} tam olarak desteklenir ve model yalnızca o üç değerden birini emit eder.

Strict Mode vs Non-Strict

python
# Strict mode — guaranteed conformance, tighter schema rules
response_format_strict = {
    "type": "json_schema",
    "json_schema": {
        "name": "product_extract",
        "strict": True,   # <-- the key flag
        "schema": product_schema
    }
}

# Non-strict — more schema flexibility, best-effort conformance
response_format_lenient = {
    "type": "json_schema",
    "json_schema": {
        "name": "product_extract",
        "strict": False,
        "schema": product_schema
    }
}

strict: True ile OpenAI, şemanı ilk kullanıldığında ön-işlemeye tabi tutar ve kısıtlı decoder'ı cache'ler. Yeni bir şemayla ilk çağrı biraz daha uzun sürer; aynı şemayla sonraki çağrılar hızlıdır. Karşılığında aldığın şey: model çıktısı yapısal olarak garantilidirjson.loads()'ı çağırıp defansif kontroller olmadan alanlara doğrudan erişebilirsin. Vazgeçtiklerin: $ref, yapısal varyantlar arasında anyOf ve özyinelemeli şemalar desteklenmez. Non-strict mode daha geniş bir JSON Schema özellik yelpazesini kabul eder ama best-effort'a düşer — model şemayı takip etmeye çalışır ama token seviyesinde kısıtlanmaz. Production çıkarma pipeline'ları için her zaman strict mode kullan. Bir kez anladığında şema kısıtlamaları yönetilebilirdir.

İç İçe Object'ler ve Array'ler

İç içe yapılar iyi çalışır, ancak her iç içe object kendi "additionalProperties": false'ına ve tüm property'leri listeleyen kendi "required" array'ine ihtiyaç duyar. Yaygın bir hata, strict kurallarını root object'e uygulayıp çocukları unutmaktır — OpenAI şemayı bir validation error ile reddeder.

python
from openai import OpenAI
import json

client = OpenAI()

order_schema = {
    "type": "object",
    "properties": {
        "order_id": {"type": "string"},
        "customer": {
            "type": "object",
            "properties": {
                "name":  {"type": "string"},
                "email": {"type": "string"}
            },
            "required": ["name", "email"],
            "additionalProperties": False   # required on nested objects too
        },
        "line_items": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "description": {"type": "string"},
                    "quantity":    {"type": "integer"},
                    "unit_price":  {"type": "number"}
                },
                "required": ["description", "quantity", "unit_price"],
                "additionalProperties": False  # required on array item schemas too
            }
        },
        "total_usd": {"type": "number"}
    },
    "required": ["order_id", "customer", "line_items", "total_usd"],
    "additionalProperties": False
}

response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[{
        "role": "user",
        "content": (
            "Parse this order: Order #ORD-5531 for Jane Smith ([email protected]). "
            "2x Wireless Keyboard at $49.99 each, 1x USB Hub at $29.99. Total: $129.97"
        )
    }],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "order_extract",
            "strict": True,
            "schema": order_schema
        }
    }
)

order = json.loads(response.choices[0].message.content)
for item in order["line_items"]:
    print(f"${item['unit_price']:.2f} x{item['quantity']}  {item['description']}")

Reddetmeleri (Refusal) Ele Almak

Structured Outputs ile bile model cevap vermeyi reddedebilir — tipik olarak prompt bir içerik politikasını tetiklediğinde (zararlı bir şeyden veri çıkarmayı istemek gibi). Bu olduğunda, finish_reason "stop"'tur ama message.content null'dur ve message.refusal reddetme metnini içerir. Bunu kontrol etmezsen, json.loads(None) çağırmaya çalıştığında AttributeError alırsın. Ayrıca finish_reason == "length"'e dikkat et — cevap max_tokens nedeniyle kesildiyse, Structured Outputs'tan bağımsız olarak JSON eksik ve parse edilemez olur.

python
import json
from openai import OpenAI

client = OpenAI()

def extract_invoice(raw_text: str) -> dict | None:
    response = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=[{"role": "user", "content": f"Extract invoice fields: {raw_text}"}],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "invoice_extract",
                "strict": True,
                "schema": invoice_schema
            }
        },
        max_tokens=1024
    )

    choice = response.choices[0]

    if choice.finish_reason == "length":
        raise ValueError("Response truncated — increase max_tokens or simplify your schema")

    if choice.message.refusal:
        # Model refused to answer — log and return None rather than crashing
        print(f"Model refused: {choice.message.refusal}")
        return None

    return json.loads(choice.message.content)


result = extract_invoice("Invoice #2024-441 from BuildRight Inc., due $3,200 by Dec 15")
if result:
    print(result["vendor_name"], result["total_amount"])

Aynı Şemayı Doğrulama için Kullanmak

Yeterince kullanılmayan bir pattern: OpenAI'ye gönderdiğin aynı JSON Schema'yı diğer kaynaklardan — webhook'lar, dosya yüklemeleri, üçüncü taraf API'ler — gelen veriyi de doğrulamak için kullan. Bu sana veri şeklin için tek bir gerçeklik kaynağı verir. Python'da jsonschema kütüphanesini kullan. Node.js'de Ajv'yi kullan. Kod yazmadan hızlı manuel bir sağlık kontrolü yapmak için şemanı JSON Validator'a da yapıştırabilirsin.

python
import json
import jsonschema
from jsonschema import validate, ValidationError

# The same schema used in your OpenAI call
invoice_schema = {
    "type": "object",
    "properties": {
        "vendor_name":    {"type": "string"},
        "invoice_number": {"type": "string"},
        "total_amount":   {"type": "number"}
    },
    "required": ["vendor_name", "invoice_number", "total_amount"],
    "additionalProperties": False
}

def validate_invoice(data: dict) -> bool:
    try:
        validate(instance=data, schema=invoice_schema)
        return True
    except ValidationError as e:
        print(f"Validation failed: {e.message}")
        print(f"  Path: {' -> '.join(str(p) for p in e.path)}")
        return False


# Validate a payload from a webhook — same schema, zero extra work
webhook_payload = json.loads(request_body)
if not validate_invoice(webhook_payload):
    return HTTPResponse(status=400, body="Invalid invoice payload")

# Validate the OpenAI output too, for belt-and-suspenders safety
llm_output = json.loads(openai_response.choices[0].message.content)
assert validate_invoice(llm_output), "LLM output failed schema validation — check schema definition"
print(f"Processing invoice {llm_output['invoice_number']} for ${llm_output['total_amount']}")
Şemanı daha hızlı oluştur: Şemanı görsel olarak oluşturmak ve rafine etmek için JSON Schema Generator'ı kullan — örnek bir JSON objesi yapıştır, başlangıç şemasını otomatik olarak oluştursun. Sonucu doğrudan OpenAI response_format'una kopyala.

JavaScript / Node.js Sürümü

OpenAI Node.js SDK Python API'sini neredeyse birebir yansıtır. Ana fark, strict'in aynı şekilde json_schema object'inin içinde yer almasıdır ve cevabı JSON.parse() ile parse edersin. Ajv ile doğrulama, Python'un jsonschema kütüphanesinin Node.js karşılığıdır — daha hızlıdır ve mükemmel TypeScript desteği vardır.

js
import OpenAI from "openai";
import Ajv from "ajv";

const client = new OpenAI();
const ajv = new Ajv();

const invoiceSchema = {
  type: "object",
  properties: {
    vendor_name:    { type: "string" },
    invoice_number: { type: "string" },
    total_amount:   { type: "number" },
    line_items: {
      type: "array",
      items: {
        type: "object",
        properties: {
          description: { type: "string" },
          amount:      { type: "number" }
        },
        required: ["description", "amount"],
        additionalProperties: false
      }
    }
  },
  required: ["vendor_name", "invoice_number", "total_amount", "line_items"],
  additionalProperties: false
};

const validateInvoice = ajv.compile(invoiceSchema);

async function extractInvoice(rawText) {
  const response = await client.chat.completions.create({
    model: "gpt-4o-2024-08-06",
    messages: [{ role: "user", content: `Extract invoice fields: ${rawText}` }],
    response_format: {
      type: "json_schema",
      json_schema: {
        name: "invoice_extract",
        strict: true,
        schema: invoiceSchema
      }
    }
  });

  const choice = response.choices[0];

  if (choice.message.refusal) {
    throw new Error(`Model refused: ${choice.message.refusal}`);
  }

  const data = JSON.parse(choice.message.content);

  // Validate even though strict mode guarantees structure —
  // useful for catching schema drift between environments
  if (!validateInvoice(data)) {
    console.error("Schema validation errors:", validateInvoice.errors);
    throw new Error("Output failed schema validation");
  }

  return data;
}

const invoice = await extractInvoice(
  "Invoice #INV-881 from Nordic Parts AS. " +
  "3x Brake Pads at $28.50 each. Total: $85.50"
);

console.log(`${invoice.vendor_name} — Invoice ${invoice.invoice_number}`);
invoice.line_items.forEach(item =>
  console.log(`  ${item.description}: $${item.amount}`)
);

Toparlayalım

Structured Outputs, tüm bir production bug kategorisini ortadan kaldırır — modelin sabah 2'de parser'ını bozan neredeyse-doğru JSON döndürdüğü tür. İş akışı basittir: şemanı dikkatli tasarla (her iç içe object'in additionalProperties: false'a ve tam bir required array'e ihtiyacı vardır), strict: true'u ayarla ve reddetmeleri ve kesilmeleri açıkça ele al. Şema yerleştiğinde tüm stack'inde yeniden kullanabilirsin — OpenAI çağrısında, webhook doğrulamasında, test fixture'larında — jsonschema (Python) veya Ajv (Node.js) gibi kütüphanelerle. Şema sıfırdan başlıyorsan, bir örnek payload'dan çalışan bir temel şema elde etmenin en hızlı yolu JSON Schema Generator'dır. Güvenilir JSON çıktısına giden yolda prompt-engineering yapma günleri bitti — bunun için yapılmış aracı kullan.