Structured output
Structured output is the same turn primitive with a stronger boundary contract.
You still run one model turn. The difference is that the schema crosses the boundary twice: first as JSON Schema sent to the provider, then as an Effect Schema validator run locally after the turn lands. The provider is asked to produce the shape; your application still checks before trusting it.
Scenario. Ask for a recipe and receive typed data, not prose.
The Contract
const Recipe = Schema.Struct({ title: Schema.String, ingredients: Schema.Array(Schema.String), prepMinutes: Schema.Number,})type Recipe = typeof Recipe.Type
const recipeFormat = StructuredFormat.fromEffectSchema(Recipe)StructuredFormat.fromEffectSchema adapts the Effect Schema into the two
things this boundary needs:
- a provider-facing JSON Schema constraint;
- a local decoder for the assembled model output.
One Turn, Typed Result
const program = Effect.gen(function* () { const turn = yield* runTurn({ history: [Items.userText("Give me a recipe for one-pan lemon chicken.")], model: "gpt-5.4-mini", // The provider sees JSON Schema; your app keeps the Effect Schema decoder. structured: recipeFormat, })
const recipe: Recipe = yield* Turn.toStructured(turn, recipeFormat) yield* Effect.logInfo("recipe", { recipe })})The request is still just a normal LanguageModel turn. The structured
option constrains generation across OpenAI, Anthropic, and Gemini providers.
Turn.toStructured then validates the final assembled text and returns typed
data or a typed failure.
Failure Is Data Too
Structured output can fail in distinct ways:
RefusalRejected: the assistant refused instead of producing output.JsonParseError: the assembled text was not valid JSON.StructuredDecodeError: the JSON did not match the schema.
Those failures stay in the Effect error channel, so callers decide whether to retry, fall back, ask a repair model, or surface the problem.
Multi-object output
For multiple items in a single response, wrap the array in an object:
const RecipeList = Schema.Struct({ recipes: Schema.Array(Recipe) })All three providers require the top-level schema to be type: object,
so a bare Schema.Array(Recipe) is rejected at the wire.
For streaming multi-object output (one object decoded as soon as its JSON is complete), see the Streaming structured output recipe.
Where you’ll reach for this
Any time you want a value back, not prose to parse:
- Extraction and classification. Pull a contact, an invoice, or a support-ticket category out of free text and hand the rest of your program a typed record it can trust.
- A typed decision inside a loop. When a turn has to pick a branch,
decode the choice and
switchon it instead of string-matching the model’s words. - Trying another provider.
structuredis honored by OpenAI, Anthropic, and Gemini, so switching is a layer change, not a rewrite.
When the objects should arrive one at a time as the model writes them (a live results list, a long batch you don’t want to wait out), reach for Streaming structured output.
Run it
OPENAI_API_KEY=sk-... pnpm tsx recipes/structured-output/index.ts --provider=responsesANTHROPIC_API_KEY=... pnpm tsx recipes/structured-output/index.ts --provider=anthropicGOOGLE_API_KEY=... pnpm tsx recipes/structured-output/index.ts --provider=geminiThe full source lives next to this README at
index.ts.