Structured output
What you will achieve
Section titled “What you will achieve”Pass a JSON Schema to complete(), get back a typed parsed object — no
JSON.parse, no markdown-fence stripping, no per-provider workaround.
When and why you need this
Section titled “When and why you need this”LLMs produce text. When you need structured data (an address, a product list, a sentiment score) you must constrain the model to emit valid JSON and then parse it. Without constraints the model may emit prose, wrap JSON in a code block, or drop required fields.
Providers have different mechanisms for this constraint:
- OpenAI supports
response_format: { type: 'json_schema', json_schema: { ... } }. - Anthropic has no native JSON-mode; the standard workaround is a forced tool call where the schema is wrapped in a tool definition and the model’s tool-call arguments are the structured output.
- Google supports
response_mime_type: 'application/json'withresponse_schema.
Maintaining three code paths — one per provider — in an application that might switch providers is expensive and error-prone.
Step by step
Section titled “Step by step”Step 1 — Define your schema inline
Section titled “Step 1 — Define your schema inline”import { complete } from '@combycode/llm-sdk';
const { parsed } = await complete<{ city: string; tempC: number }>({ model: process.env.LLM_MODEL!, apiKey: process.env.LLM_API_KEY, prompt: 'Extract the city and temperature in Celsius. Text: "Paris is 20 degrees C."', structured: { schema: { type: 'object', properties: { city: { type: 'string' }, tempC: { type: 'number' }, }, required: ['city', 'tempC'], additionalProperties: false, }, }, maxTokens: 64,});
console.log(parsed.city); // 'Paris'console.log(parsed.tempC); // 20The generic <T> on complete<T>() types parsed as T. TypeScript infers all
field types from your generic — no extra cast.
Step 2 — Understand what the provider receives
Section titled “Step 2 — Understand what the provider receives”Under the hood the adapter inspects provider capability:
- OpenAI receives
response_format: { type: 'json_schema', json_schema: { name, strict, schema } }. - Anthropic receives an extra tool named
structured_outputwith your schema as parameters;tool_choiceis forced to{ type: 'tool', name: 'structured_output' }. The tool call arguments are extracted transparently. - Google receives
response_mime_type: 'application/json'andresponse_schema.
This translation is automatic. You write one schema, the adapter does the right thing.
Step 3 — Use strict mode
Section titled “Step 3 — Use strict mode”When your schema is simple (no circular refs, no anyOf with nullable, all fields
required) enable strict mode for stricter enforcement on providers that support it:
const { parsed } = await complete<{ sentiment: 'positive' | 'negative' | 'neutral' }>({ model: process.env.LLM_MODEL!, apiKey: process.env.LLM_API_KEY, prompt: 'Classify the sentiment: "The product is surprisingly good."', structured: { schema: { type: 'object', properties: { sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] }, }, required: ['sentiment'], additionalProperties: false, }, strict: true, }, maxTokens: 32,});
console.log(parsed.sentiment); // 'positive'Strict mode (strict: true) maps to json_schema.strict: true on OpenAI, which
enables guaranteed-valid JSON at the cost of rejecting schemas that use features
OpenAI’s strict validator does not support (e.g. anyOf, oneOf). On Anthropic and
Google the field is silently ignored — structured output is always enforced through
tool-call or response-schema mechanisms which do not have a separate strict flag.
Step 4 — Nested and array schemas
Section titled “Step 4 — Nested and array schemas”Schemas are plain JSON Schema objects. Nested objects and arrays work as expected:
type Invoice = { vendor: string; total: number; items: Array<{ description: string; amount: number }>;};
const { parsed } = await complete<Invoice>({ model: process.env.LLM_MODEL!, apiKey: process.env.LLM_API_KEY, prompt: 'Extract invoice data from: "Acme Corp. Items: Widget $12.50, Bolt $0.99. Total $13.49"', structured: { schema: { type: 'object', properties: { vendor: { type: 'string' }, total: { type: 'number' }, items: { type: 'array', items: { type: 'object', properties: { description: { type: 'string' }, amount: { type: 'number' }, }, required: ['description', 'amount'], }, }, }, required: ['vendor', 'total', 'items'], }, }, maxTokens: 256,});Step 5 — Handle parse failures
Section titled “Step 5 — Handle parse failures”If the model somehow emits invalid JSON (network truncation, unusual model behaviour),
parsed will be undefined and text will contain the raw string. A retry with the
same schema usually succeeds:
const result = await complete<MyType>({ model: process.env.LLM_MODEL!, apiKey: process.env.LLM_API_KEY, prompt, structured: { schema }, maxTokens: 256,});
if (result.parsed === undefined) { console.warn('parse failed, raw text:', result.text); // retry or fall back}Your options
Section titled “Your options”The structured option takes three fields:
| Field | Type | Default | Notes |
|---|---|---|---|
schema | Record<string, unknown> | required | A JSON Schema object. Must be a valid JSON Schema — the SDK does not validate the schema itself, but invalid schemas will cause provider errors. |
name | string | 'structured_output' | The tool name used on Anthropic (where structured output is implemented via a forced tool call). Also used as json_schema.name on OpenAI. Use a descriptive name when debugging Anthropic tool-call logs. |
strict | boolean | false | Maps to OpenAI’s strict: true in response_format. No effect on Anthropic or Google. Only enable if your schema is compatible (no anyOf, all fields required). |
complete<T>() vs LLMClient.structuredComplete<T>()
complete<T>() with a structured option is the recommended path. It returns
{ text, parsed, usage, response } where parsed is typed as T | undefined.
LLMClient.structuredComplete<T>(input, schema, options) is a lower-level convenience
on the LLMClient class. It calls complete() internally and throws if parsing fails
(instead of returning undefined). Use it when you want the parse failure to propagate
as an exception rather than checking parsed !== undefined.
Schema compatibility across providers
Anthropic’s forced-tool workaround accepts any valid JSON Schema. OpenAI’s strict mode
rejects schemas using anyOf, oneOf, $ref, optional properties (must all be in
required), or null types without explicit union. If you target multiple providers,
design schemas conservatively: flat objects, all fields required, no anyOf.
Compare the SDKs
Section titled “Compare the SDKs”Scenario 11 (raw structured JSON):
Scenario 12 (typed structured parse):
The structural difference: official SDKs require you to use three distinct mechanisms.
With OpenAI you set response_format and call JSON.parse() or use zodResponseFormat.
With Anthropic you define a tool, force tool_choice, and extract tool_use.input from
the response content. With Google you set responseMimeType and responseSchema and
call JSON.parse() on candidates[0].content.parts[0].text. ORXA reduces this to one
structured field; the provider selection, schema translation, and JSON parsing are all
done internally.
Gotchas and next steps
Section titled “Gotchas and next steps”Empty parsed is not an error. When parsed is undefined, inspect text. If
text contains valid JSON wrapped in a markdown code block, the model ignored your
structured-output constraint. This happens more with smaller or older models. Try being
more explicit in your prompt: 'Respond with JSON only, matching this exact schema: ...'.
Do not use structured with stream(). The streaming path does not support
structured output — the model must emit the complete JSON in a single block. Use
complete() for all structured-output calls.
strict: true and optional fields. OpenAI’s strict mode requires every property to
be in required and does not allow additionalProperties: true. If you want optional
fields, mark them required and use { type: ['string', 'null'] } instead.
Next steps:
- Tool call — let the model call functions rather than returning JSON
- Reasoning — combine reasoning with structured extraction
- Quickstart — the non-structured baseline