Claude ile Structured Outputs: JSON Schema, C# Record'ları ve Doğrulama
Claude'un cevabını bir şemaya bağlamak için output_config.format'a JSON Schema verirsin; şemayı C# record'undan System.Text.Json exporter ile üretip additionalProperties: false ekler, cevabı stop_reason'a bakarak parse edersin. SDK'nın Create<T> kısıtı, nullable alanların şemadaki hâli, enum harf farkı, refusal ve max_tokens durumu ve şemaya uyan ama iş kuralını çiğneyen Bookstore iade talebi.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (System.Text.Json 10), Anthropic C# SDK 13.8.0 ve Claude API version 2026-10-21 ile derlenmiş, birim testleri çalıştırılmıştır. Yazıdaki JSON Schema çıktıları yerelde üretilmiştir; extraction testleri SDK'nın gerçek client'ını ağa çıkmayan sahte bir HttpMessageHandler ile kullanıyor. Structured outputs davranışı ve sınırları 15 Kasım 2026'da platform.claude.com belgelerinden alınmıştır. Bu yazıda resmî belgelerden yararlanılmıştır.
Önce Tahmin Et
Müşteri hizmetlerinden Gül, sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için iade formunu sadeleştirmek istiyor: müşteri derdini serbest metinle yazsın, sistem bunu sipariş numarası, iade sebebi, kitaplar ve aciliyetten oluşan bir taslağa çevirsin. Üçüncü yazıdaki sınıflandırıcı tek bir etiket döndürüyordu ve model bir cümle yazınca sonuç kayboluyordu. Emre bu kez cevabı bir JSON şemasına bağlıyor. Testte her cevap şemaya uyuyor. Sonra Alan, Ada'nın 1042 numaralı siparişindeki Dune için iade talebi yazıyor ve sistem bunu düzgün bir taslağa çeviriyor.
Tahminin ne? Şemaya uyan bir cevap doğru bir cevap mıdır? Ve müşteri sipariş numarasını hiç yazmazsa şemadaki zorunlu order_id alanına ne gelir? Structured output'un sağlayıcıdan bağımsız mantığı ve GetResponseAsync<T> ile kullanımı LLM Temelleri serisinin onuncu yazısında, extraction prompt'larının tasarımı Prompt Engineering serisinin ikinci yazısında anlatıldı. Bu yazıda Claude API'nin output_config.format parametresine, C# tiplerinden şema üretmeye ve şemanın yakalayamadığı hatalara bakıyoruz.
output_config.format: Cevabı Şemaya Bağlamak
Structured outputs sayfası iki tamamlayıcı özellik tarif ediyor. JSON outputs (output_config.format) modelin cevabını, strict tool use (strict: true) ise tool çağrılarının input'unu şemaya bağlıyor. Beşinci yazıda ikincisini kullandık; bu yazı birincisi hakkında. İkisi aynı istekte birlikte de kullanılabiliyor.
Mekanizma aynı: constrained decoding. API şemayı bir grammar'a derliyor ve model yalnız bu grammar'a uyan token'ları üretebiliyor. Belgeye göre sonuç her zaman parse edilebilen, zorunlu alanları eksiksiz ve tipleri doğru bir JSON; JSON cevabın text block'unda geliyor. İstekte şema output_config.format içinde, type: "json_schema" ile gidiyor. Effort da aynı output_config object'inde olduğu için ikisi yan yana duruyor.
Eski bir parametreye dikkat. Structured outputs bir süre beta'daydı ve parametrenin adı output_format'tı. Belgeye göre bu parametre artık deprecated; kullanmak için structured-outputs-2025-11-13 beta header'ı gerekiyor, header olmadan 400 dönüyor. Eski örneklerde output_format görürsen output_config.format'a taşı.
C# Tipinden JSON Schema Üretmek
Taslağın C# karşılığı bir record:
using System.ComponentModel;
namespace Bookstore.Api.Returns;
public enum Urgency
{
Low,
Normal,
High,
}
public sealed record ReturnItem(
[property: Description("Book title as the customer wrote it.")] string Title,
[property: Description("Number of copies to return.")] int Quantity);
public sealed record ReturnRequestDraft(
[property: Description("Order number if the customer gave one, otherwise null.")] string? OrderId,
ReturnReason Reason,
IReadOnlyList<ReturnItem> Items,
Urgency Urgency,
[property: Description("One sentence in English for the support team.")] string Summary);
Şemayı elle yazmak yerine bu tipten üretmek istiyoruz; böylece tip değişince şema da değişiyor. .NET'te bunun için System.Text.Json'ın JsonSchemaExporter'ı var. İlk denemede ASP.NET Core'un kullandığı JsonSerializerDefaults.Web ayarlarıyla bir şema ürettik:
JsonSerializerOptions web = new(JsonSerializerDefaults.Web)
{
Converters = { new JsonStringEnumConverter() },
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
};
Console.WriteLine(web.GetJsonSchemaAsNode(typeof(ReturnItem)));
{
"type": [
"object",
"null"
],
"properties": {
"title": {
"type": "string"
},
"quantity": {
"type": [
"string",
"integer"
],
"pattern": "^-?(?:0|[1-9]\\d*)$"
}
},
"required": [
"title",
"quantity"
]
}
Bu çıktı Claude için iki açıdan sorunlu. quantity hem string hem integer olabiliyor, çünkü Web varsayılanları sayıların string olarak okunmasına izin veriyor ve exporter bunu dürüstçe şemaya yazıyor; bu bir union tipi ve strict şemalarda union kullanan parametre sayısı sınırlı. Daha önemlisi object'lerde additionalProperties: false yok; belgeye göre structured outputs her object'te bunu istiyor ve başka bir değerde 400 dönüyor. Kök tipin ["object", "null"] olması da istediğimiz şey değil.
Bu yüzden exporter'ı kendi ayarlarımızla saran küçük bir yardımcı yazdık:
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Schema;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace Bookstore.Api.Ai;
// System.Text.Json ile bir C# tipinden JSON Schema üretir ve Claude'un structured outputs alt kümesine uyarlar.
public static class ClaudeJsonSchema
{
// JsonSerializerDefaults.Web değil: o, sayıların string gelmesine izin verir ve şema da bunu söyler.
public static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },
RespectNullableAnnotations = true,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
};
private static readonly JsonSchemaExporterOptions ExporterOptions = new()
{
TreatNullObliviousAsNonNullable = true,
TransformSchemaNode = (context, schema) =>
{
if (schema is not JsonObject node)
{
return schema;
}
// Claude her object'te additionalProperties: false ister.
if (IsObjectSchema(node))
{
node["additionalProperties"] = false;
}
// Property'deki [Description] şemanın description'ı olur; model bunu okur.
DescriptionAttribute? description = context.PropertyInfo?.AttributeProvider?
.GetCustomAttributes(typeof(DescriptionAttribute), inherit: true)
.OfType<DescriptionAttribute>()
.FirstOrDefault();
if (description is not null)
{
node["description"] = description.Description;
}
return node;
},
};
public static JsonObject For<T>() =>
(JsonObject)SerializerOptions.GetJsonSchemaAsNode(typeof(T), ExporterOptions);
public static Dictionary<string, JsonElement> AsSchemaDictionary<T>() =>
For<T>().ToDictionary(kv => kv.Key, kv => JsonSerializer.SerializeToElement(kv.Value));
private static bool IsObjectSchema(JsonObject node) => node["type"] switch
{
JsonValue value => value.GetValue<string>() == "object",
JsonArray types => types.Any(t => t?.GetValue<string>() == "object"),
_ => false,
};
}
Üç ayar işi görüyor. Web varsayılanlarını kullanmıyoruz, böylece sayılar yalnız sayı. TreatNullObliviousAsNonNullable nullable annotation'ı olmayan referans tiplerini null'a kapatıyor. TransformSchemaNode her object'e additionalProperties: false ekliyor ve property'deki [Description] attribute'unu şemanın description alanına taşıyor; model bu açıklamaları okuyor. İsimler ve enum değerleri snake_case, çünkü deserialize ederken de aynı SerializerOptions kullanılıyor. Sonuç:
{
"type": "object",
"properties": {
"order_id": {
"type": [
"string",
"null"
],
"description": "Order number if the customer gave one, otherwise null."
},
"reason": {
"enum": [
"damaged",
"wrong_item",
"not_as_described",
"changed_mind",
"late_delivery",
"other"
]
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Book title as the customer wrote it."
},
"quantity": {
"type": "integer",
"description": "Number of copies to return."
}
},
"required": [
"title",
"quantity"
],
"additionalProperties": false
}
},
"urgency": {
"enum": [
"low",
"normal",
"high"
]
},
"summary": {
"type": "string",
"description": "One sentence in English for the support team."
}
},
"required": [
"order_id",
"reason",
"items",
"urgency",
"summary"
],
"additionalProperties": false
}
OrderId, C# tarafında string? olduğu için şemada ["string", "null"]. Önce Tahmin Et'teki ikinci sorunun cevabı burada: alan zorunlu ama null olabilir, ve açıklama "müşteri vermediyse null" diyor. Alan null olamasaydı model, şemaya uymak için bir sipariş numarası üretmek zorunda kalırdı.
SDK'nın Kendi Yardımcısı
C# SDK'nın da bir yolu var. Structured outputs sayfasındaki C# örneği bir tipi generic Create<T>() overload'una veriyor; SDK tipten şema türetiyor, output_config.format olarak gönderiyor ve her text block'u tipe parse ediyor:
StructuredMessage<ReturnRequestDraft> response = await claude.Messages.Create<ReturnRequestDraft>(new MessageCreateParams
{
Model = Model.ClaudeHaiku5_5,
MaxTokens = 2048,
Messages = [new UserMessageParam { Content = text }],
});
Bunu kendi record'umuzla derlediğimizde derleyici şunu verdi:
error CS0310: 'ReturnRequestDraft' must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'T' in the generic type or method 'MessageServiceExtensions.Create<T>(IMessageService, MessageCreateParams, CancellationToken)'
SDK 13.8.0'da Create<T>, T için parametresiz bir constructor istiyor. Belgedeki örneğin { get; set; } property'leri olan bir record kullanması bu yüzden; bizim positional record'umuz bu kısıta uymuyor. SDK'nın şema üreten metodu (StructuredOutput.ToJsonSchema<T>()) ise aynı record'la çalışıyor. Çıktısının başı:
StructuredOutput.ToJsonSchema<ReturnRequestDraft>()
{
"type": "object",
"properties": {
"orderId": {
"type": "string"
},
"reason": {
"enum": [
"damaged",
"wrong_item",
"not_as_described",
"changed_mind",
"late_delivery",
"other"
]
},
...
},
"required": [
"orderId",
"reason",
"items",
"urgency",
"summary"
],
"additionalProperties": false
}
Burada önemli bir fark var: SDK, string? olan OrderId için şemaya null'ı eklemedi ve [Description] açıklamalarını taşımadı. Bu şemayla model, müşteri numara vermemiş olsa bile orderId alanına bir string yazmak zorunda. Bu davranış SDK sürümüne göre değişebilir; önemli olan, gönderdiğin şemayı görmek ve test etmek. Biz Bookstore'da kendi exporter'ımızı kullanıyoruz: aynı serializer ayarları API'nin geri kalanıyla tutarlı, şema bir testte sabit ve nullable alanlar istediğimiz gibi.
Bookstore: ReturnRequestExtractor
Şemayı bir kez kurup her istekte kullanıyoruz:
using System.Text.Json;
using Anthropic;
using Anthropic.Models.Messages;
using Bookstore.Api.Ai;
using Microsoft.Extensions.Options;
namespace Bookstore.Api.Returns;
public sealed record ExtractionResult(ReturnRequestDraft? Draft, ReplyOutcome Outcome, string? Problem, TokenUsage Usage);
public sealed class ReturnRequestExtractor(IAnthropicClient claude, IOptions<ClaudeOptions> options)
{
private const string SystemPrompt = """
You turn a customer's free-text return request for Bookstore, an online bookstore, into the given JSON structure.
Use only what the customer wrote. If the order number is not in the text, use null; never invent one.
Urgency is high only if the customer mentions a deadline or a damaged gift.
""";
// Bir kez kurulur: API şemayı bir grammar'a derler ve 24 saat cache'ler.
private static readonly JsonOutputFormat Format = new() { Schema = ClaudeJsonSchema.AsSchemaDictionary<ReturnRequestDraft>() };
public MessageCreateParams BuildRequest(string customerText) => new()
{
Model = options.Value.Model,
MaxTokens = 2048,
System = SystemPrompt,
OutputConfig = new OutputConfig { Format = Format, Effort = Effort.Low },
Messages = [new UserMessageParam { Content = customerText }],
};
public async Task<ExtractionResult> ExtractAsync(string customerText, CancellationToken cancellationToken)
{
Message message = await claude.Messages.Create(BuildRequest(customerText), cancellationToken);
ClaudeReply reply = ClaudeReplyReader.Read(message);
// Refusal ve max_tokens şemaya uymayan bir metin bırakabilir; parse etme.
if (reply.Outcome != ReplyOutcome.Complete)
{
return new ExtractionResult(null, reply.Outcome, $"No structured output ({reply.Outcome}).", reply.Usage);
}
try
{
ReturnRequestDraft? draft = JsonSerializer.Deserialize<ReturnRequestDraft>(reply.Text, ClaudeJsonSchema.SerializerOptions);
return new ExtractionResult(draft, reply.Outcome, draft is null ? "Empty output." : null, reply.Usage);
}
catch (JsonException ex)
{
return new ExtractionResult(null, reply.Outcome, $"Output did not deserialize: {ex.Message}", reply.Usage);
}
}
}
Structured outputs sayfası grammar'ın nasıl cache'lendiğini anlatıyor: belirli bir şemanın ilk kullanımında grammar derlendiği için ek latency oluyor, derlenen grammar son kullanımdan itibaren 24 saat cache'te kalıyor. Şemanın yapısını değiştirmek bu cache'i geçersiz kılıyor; yalnız name ya da description değiştirmek kılmıyor. Belge iki maliyet notu daha düşüyor: structured outputs kullanınca modele formatı anlatan ek bir system prompt ekleniyor ve input token'ı biraz artıyor; output_config.format'ı değiştirmek de o konuşmanın prompt cache'ini bozuyor. Şemayı static bir alanda tutmanın sebebi bu.
Cevabı parse etmeden önce stop_reason'a bakıyoruz. Belgenin "Invalid outputs" bölümü, şemaya uyumun garanti olmadığı iki durumu sayıyor: refusal'da reddetme mesajı şemanın önüne geçebiliyor, max_tokens'ta çıktı yarım kalabiliyor. İkisinde de text'i parse etmiyoruz. Belgeye göre refusal'da ayrıca üretilen token'lar faturalanıyor.
Bir de enum notu var. Belgeye göre structured outputs enum ve const değerlerinin büyük-küçük harfini garanti etmiyor; model şemadakinden yalnız harf büyüklüğünde farklı bir değer dönebiliyor ve cevap hatasız tamamlanıyor. JsonStringEnumConverter okurken büyük-küçük harfe duyarsız davrandığı için "Damaged" da ReturnReason.Damaged'e dönüşüyor; bunu bir testle sabitledik.
Şemaya Uyan ama Yanlış: İş Kuralları Kodda
Alan'ın talebine dönelim. Model müşterinin yazdığını doğru çıkardı: sipariş 1042, sebep damaged, kitap Dune, adet 1. JSON şemaya uyuyor. Sorun JSON'da değil, gerçekte: 1042 Alan'ın siparişi değil. Şema bir veri şeklini garanti ediyor, verinin doğruluğunu değil. Bu yüzden taslak, kodda çalışan bir iş kuralı kontrolünden geçiyor:
using Bookstore.Api.Catalog;
using Bookstore.Api.Ordering;
namespace Bookstore.Api.Returns;
// Şemaya uyan bir taslak yine de iş kurallarını çiğneyebilir; bu kontroller kodda çalışır.
public sealed class ReturnPolicy(IOrderRepository orders, IBookRepository books)
{
public IReadOnlyList<string> Check(ReturnRequestDraft draft, CurrentCustomer customer)
{
List<string> problems = [];
if (draft.OrderId is null)
{
problems.Add("The order number is missing; ask the customer for it.");
return problems;
}
Order? order = orders.Find(draft.OrderId);
if (order is null || order.CustomerId != customer.Id)
{
problems.Add($"Order {draft.OrderId} was not found for this customer.");
return problems;
}
if (order.Status != "Shipped" && order.Status != "Delivered")
{
problems.Add($"Order {order.Id} is '{order.Status}'; it can be cancelled, not returned.");
}
if (draft.Items.Count == 0)
{
problems.Add("No items were named.");
}
HashSet<string> titlesInOrder = order.Isbns
.Select(books.FindByIsbn)
.Where(b => b is not null)
.Select(b => b!.Title)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (ReturnItem item in draft.Items)
{
if (!titlesInOrder.Contains(item.Title))
{
problems.Add($"'{item.Title}' is not in order {order.Id}.");
}
if (item.Quantity < 1)
{
problems.Add($"'{item.Title}' has an invalid quantity ({item.Quantity}).");
}
}
return problems;
}
}
Kurallar Bookstore'un iade politikasından: sipariş bu müşteriye ait olmalı, kargoya verilmiş olmalı (hazırlanan bir sipariş iade edilmiyor, iptal ediliyor), iade edilen kitaplar o siparişte olmalı ve adet en az 1. Beşinci yazıdaki get_order_status tool'unda olduğu gibi, başka bir müşterinin siparişi "bulunamadı" olarak görünüyor.
Endpoint hiçbir şey oluşturmuyor; taslağı ve sorun listesini dönüyor:
app.MapPost("/api/returns/extract", async Task<IResult> (
ReturnText body, ClaimsPrincipal user, ReturnRequestExtractor extractor, ReturnPolicy policy, CancellationToken ct) =>
{
if (!Guid.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier), out Guid customerId))
{
return TypedResults.Unauthorized();
}
ExtractionResult result = await extractor.ExtractAsync(body.Text, ct);
if (result.Draft is null)
{
return TypedResults.Ok(new ReturnCheck(null, [result.Problem ?? "No draft."]));
}
// Burada hiçbir şey oluşturulmaz: taslak ve sorunları onay için müşteriye ya da destek ekibine gider.
return TypedResults.Ok(new ReturnCheck(result.Draft, policy.Check(result.Draft, new CurrentCustomer(customerId))));
});
return app;
}
}
Taslak, müşteriye ya da Gül'ün ekibine onay için gösteriliyor. Model çıktısının bir karar değil bir öneri olduğu bu tasarımın LLM Uygulama Güvenliği serisinin beşinci yazısındaki "LLM çıktısı güvenilmeyen input'tur" ilkesiyle bağı açık.
Testler
Şemanın kapalı olduğunu, isteğin şemayı gerçekten taşıdığını ve yarım çıktının parse edilmediğini test ediyoruz. Sahte cevaplar text block'unda şu JSON'u taşıyor:
{"order_id":"1042","reason":"damaged","items":[{"title":"Dune","quantity":1}],"urgency":"normal","summary":"Dune arrived with a torn cover."}
using System.Text.Json;
using System.Text.Json.Nodes;
using Bookstore.Api.Ai;
using Bookstore.Api.Catalog;
using Bookstore.Api.Ordering;
using Bookstore.Api.Returns;
using Microsoft.Extensions.Options;
namespace Bookstore.Api.Tests;
public sealed class StructuredOutputTests
{
private static string Reply(string stopReason, string text) => $$$"""
{"id":"msg_01","type":"message","role":"assistant","model":"claude-haiku-5-5",
"content":[{"type":"text","text":{{{JsonSerializer.Serialize(text)}}}}],
"stop_reason":"{{{stopReason}}}","stop_sequence":null,"usage":{"input_tokens":610,"output_tokens":85}}
""";
private const string DraftJson = """
{"order_id":"1042","reason":"damaged","items":[{"title":"Dune","quantity":1}],"urgency":"normal","summary":"Dune arrived with a torn cover."}
""";
private static ReturnRequestExtractor CreateExtractor(FakeClaudeHandler handler) =>
new(handler.CreateClient(), Options.Create(new ClaudeOptions()));
private static readonly ReturnPolicy Policy = new(new InMemoryOrderRepository(), new InMemoryBookRepository());
[Fact]
public void Schema_IsClosedOnEveryObject()
{
JsonObject schema = ClaudeJsonSchema.For<ReturnRequestDraft>();
Assert.False(schema["additionalProperties"]!.GetValue<bool>());
Assert.False(schema["properties"]!["items"]!["items"]!["additionalProperties"]!.GetValue<bool>());
Assert.Equal(5, schema["required"]!.AsArray().Count);
}
[Fact]
public async Task ExtractAsync_SendsTheSchemaAndParsesTheText()
{
FakeClaudeHandler handler = new(Reply("end_turn", DraftJson));
ExtractionResult result = await CreateExtractor(handler).ExtractAsync(
"Order 1042: Dune came with a torn cover, I want to send it back.", CancellationToken.None);
Assert.Equal(new ReturnItem("Dune", 1), Assert.Single(result.Draft!.Items));
Assert.Equal(ReturnReason.Damaged, result.Draft.Reason);
JsonElement format = JsonDocument.Parse(handler.RequestBodies[0]).RootElement
.GetProperty("output_config").GetProperty("format");
Assert.Equal("json_schema", format.GetProperty("type").GetString());
Assert.False(format.GetProperty("schema").GetProperty("additionalProperties").GetBoolean());
}
[Theory]
[InlineData("refusal")]
[InlineData("max_tokens")]
public async Task ExtractAsync_DoesNotParseIncompleteOutput(string stopReason)
{
FakeClaudeHandler handler = new(Reply(stopReason, """{"order_id":"1042","reason":"dam"""));
ExtractionResult result = await CreateExtractor(handler).ExtractAsync("...", CancellationToken.None);
Assert.Null(result.Draft);
Assert.NotNull(result.Problem);
}
}
İş kuralı testleri, şemaya uyan aynı taslağın Ada için geçerli, Alan için geçersiz olduğunu gösteriyor:
[Fact]
public void Deserialize_EnumCasingDifferences_StillParse()
{
// Belgeye göre enum değerlerinin büyük-küçük harfi garanti değil.
ReturnRequestDraft? draft = JsonSerializer.Deserialize<ReturnRequestDraft>(
DraftJson.Replace("\"damaged\"", "\"Damaged\""), ClaudeJsonSchema.SerializerOptions);
Assert.Equal(ReturnReason.Damaged, draft!.Reason);
}
[Fact]
public void Policy_SchemaValidDraftForSomeoneElsesOrder_IsRejected()
{
ReturnRequestDraft draft = JsonSerializer.Deserialize<ReturnRequestDraft>(DraftJson, ClaudeJsonSchema.SerializerOptions)!;
Assert.Empty(Policy.Check(draft, new CurrentCustomer(InMemoryOrderRepository.Ada)));
Assert.Equal(["Order 1042 was not found for this customer."], Policy.Check(draft, new CurrentCustomer(InMemoryOrderRepository.Alan)));
}
[Fact]
public void Policy_BookNotInTheOrder_IsReported()
{
ReturnRequestDraft draft = new("1042", ReturnReason.WrongItem, [new ReturnItem("Emma", 1)], Urgency.Normal, "Wrong book.");
Assert.Equal(["'Emma' is not in order 1042."], Policy.Check(draft, new CurrentCustomer(InMemoryOrderRepository.Ada)));
}
Passed! - Failed: 0, Passed: 28, Skipped: 0, Total: 28, Duration: 55 ms - Bookstore.Api.Tests.dll (net10.0)
Kısıtlar ve Uyumluluk
Belgedeki kısıtların Bookstore için önemli olanları şunlar.
Desteklenen JSON Schema alt kümesi: temel tipler, enum, const, anyOf, allOf ($ref ile değil), yerel $ref ve definitions, required, additionalProperties: false ve date-time, date, email, uuid gibi belirli string formatları. Desteklenmeyenler: recursive şemalar, minimum ve maximum gibi sayı sınırları, minLength ve maxLength, minItems için 0 ve 1 dışındaki değerler. Desteklenmeyen bir özellik 400 döndürüyor. Belge, bazı SDK yardımcılarının bu sınırları şemadan çıkarıp açıklamaya yazdığını ve cevabı orijinal şemaya göre kendisi doğruladığını söylüyor; biz bu sınırları kodda, ReturnPolicy gibi kontrollerle uyguluyoruz.
Karmaşıklık sınırları: output_config.format ya da strict: true kullanan bir istekte, bütün strict şemalarda toplam 24 opsiyonel parametre ve union tipi kullanan en fazla 16 parametre. Bizim taslağımızda bütün alanlar zorunlu, tek union order_id'deki ["string", "null"].
Property sırası: çıktıda zorunlu alanlar önce, opsiyonel alanlar sonra geliyor. Sıra önemliyse bütün alanları zorunlu yap.
Uyumsuzluklar: citations ile birlikte kullanılamıyor; ikisi birlikte açılırsa API 400 dönüyor. Prefill ile de uyumsuz. Batch, token counting ve streaming ile çalışıyor. Bir de model notu: Fable 5.2 belgesine göre customer-managed encryption keys kullanan organizasyonlarda Fable 5.2 ve Mythos 5.2 için structured outputs (strict tool use dahil) yok.
Üretimde Dikkat
Şemayı sürümle. Şema bir API sözleşmesi gibi; bir alanın adını değiştirmek hem grammar cache'ini hem de taslağı okuyan kodu etkiliyor. Şemayı bir testle sabitlemek, değişikliği bilinçli hâle getiriyor.
Şemaya uymayı başarı saymamak. Refusal, max_tokens ve iş kuralı hatalarını ayrı metriklerle izle. "Parse edildi" ile "iade oluşturuldu" arasında bir onay adımı olmalı.
Kişisel veri şemaya girmez. Belgeye göre şemalar prompt'lardan ayrı cache'leniyor; HIPAA (ABD'nin sağlık verisi düzenlemesi) kapsamındaki veriler için belge şemaya hassas veri konmamasını açıkça yazıyor. Aynı ilke Bookstore için de geçerli: enum değerlerine ya da açıklamalara müşteri adı, adres veya sipariş içeriği koyma.
Maliyet. Ek system prompt input token'ını biraz artırıyor, effort'u düşük tutmak output'u kısa tutuyor. Extraction gibi iyi tanımlanmış bir iş için üçüncü yazıdaki değerlendirme aracıyla low effort'u doğrula.
Sık Düşülen Tuzaklar
"Şemaya uydu, demek ki doğru" sanmak. Şema şekli garanti eder; sahiplik, stok ve tarih gibi kuralları kod kontrol eder.
Yarım JSON'u parse etmeye çalışmak. stop_reason end_turn değilse text'e güvenme.
Nullable alanı şemada zorunlu string yapmak. Model eksik bilgiyi uydurmak zorunda kalır. Gönderdiğin şemayı her zaman gör.
JsonSerializerDefaults.Web ile şema üretmek. Sayılar string olabilir hâle gelir ve additionalProperties eksik kalır.
Eski output_format parametresini kullanmak. Deprecated; output_config.format kullan.
Enum'u büyük-küçük harfe duyarlı karşılaştırmak. Belge harf büyüklüğünü garanti etmiyor.
Küçük Bir Deneme
★☆☆ ReturnRequestDraft'a DateOnly? DeliveredOn alanı ekle. Exporter'ın ürettiği şemada bu alanın format'ına bak ve belgede desteklenen string formatlarıyla karşılaştır.
★★☆ ClaudeJsonSchema için, desteklenmeyen anahtar kelimeler (minimum, maximum, minLength, maxLength, pattern) şemada kalırsa testin kırılmasını sağlayan bir kontrol yaz. ReturnItem.Quantity'ye [Range(1, 10)] ekleyip exporter'ın bunu şemaya yazıp yazmadığını gör.
★★★ Üçüncü yazıdaki ReturnClassifier'ı structured outputs ile yeniden yaz: yalnız reason alanı olan bir record. Değerlendirme aracını iki sürümle çalıştırıp ParseLabel'ın null döndüğü durumların kaybolduğunu ölç.
Bu yazıyla Messages API'nin kemik yapısını tamamladık: istek, cevap, model ve effort, streaming, tool use ve structured outputs. Yedinci yazıda maliyete dönüyoruz: prompt caching ile tekrar eden system prompt'u, tool tanımlarını ve konuşma geçmişini cache'lemek, cache breakpoint'leri, TTL'ler (cache'in ne kadar süre geçerli kalacağı) ve cache_read_input_tokens ile cache'in gerçekten çalıştığını doğrulamak.