# Structured Outputs
Get a **typed, schema-validated reply** instead of free text. The JSON Schema
is enforced natively by the provider — not by prompt begging.
```rust
use yoagent::{Agent, provider::ModelConfig};
#[derive(serde::Deserialize)]
struct Invoice {
vendor: String,
total_cents: u64,
line_items: Vec<String>,
}
let mut agent = Agent::from_config(ModelConfig::claude_sonnet_5());
let invoice: Invoice = agent
.prompt_structured(
"Extract the invoice from the attached text: ...",
serde_json::json!({
"type": "object",
"properties": {
"vendor": {"type": "string"},
"total_cents": {"type": "integer"},
"line_items": {"type": "array", "items": {"type": "string"}}
},
"required": ["vendor", "total_cents", "line_items"],
"additionalProperties": false
}),
)
.await?;
```
Derive the schema however you like — by hand as above, or with the
[`schemars`](https://crates.io/crates/schemars) crate (convert with
`serde_json::to_value(schemars::schema_for!(Invoice))`). Mind the provider
dialects: OpenAI strict mode requires `additionalProperties: false` and every
property listed in `required`; Anthropic's native JSON outputs also require
`additionalProperties: false` on objects and reject numeric/length constraints
(`minimum`, `maxLength`, …); Gemini rejects `$defs`/`$ref`. Schemas are
passed through as given.
## How each provider enforces it
| Anthropic, `native_structured_output` on (every `ModelConfig::claude_*` preset, and `opencode_zen("claude-…")` ids from Claude 4.5 on) | `output_config.format: {type: "json_schema", schema}` — the reply's text block is constrained to the schema; no tool is forced, thinking stays on, and the key shares `output_config` with the thinking `effort` |
| Anthropic, flag off (default, e.g. a bare `ModelConfig::anthropic(..)`) | Forced tool call — a synthetic tool is built from your schema and `tool_choice` forces it; the loop unwraps the call back into text |
| OpenAI-compatible | `response_format: {type: "json_schema", strict: true}` |
| Google Gemini | `generationConfig.responseSchema` + JSON mime type (note: Gemini uses an OpenAPI-style schema dialect — your schema is passed through as given) |
| OpenAI Responses / Azure / Vertex / Bedrock | Not yet wired — a warning is logged and the model replies as free text, which still must parse into `T` |
## Semantics & caveats
- `prompt_structured` runs the loop to completion internally and returns the
parsed `T` — there is no event receiver for this call.
- Three error shapes: `Provider { message }` when the API call itself failed
(auth, network, a schema-induced 400 — retrying the parse is pointless);
`Parse { source, raw }` when the model's text didn't deserialize (the raw
text is preserved so you can retry or salvage); `NoOutput` when the run
produced no text. Only messages produced by **this call** are considered —
stale output from earlier turns is never parsed.
- On Anthropic's tool-forcing path the forced tool call preempts regular
tools for that request, and **disables extended thinking** for that request
(forced tool choice and thinking are mutually exclusive at the API level —
a warning is logged). Treat such structured prompts as
**extraction/finalization calls**, not agentic tool-using turns. The native
path has neither restriction: tool choice stays `auto`, so the model may
call your tools first, and the last text block is parsed. The loop's
unwrap of the synthetic call runs only on the tool-forcing path, so on the
native path a tool of yours that happens to share the schema's name is
executed normally.
- **Claude Fable 5.1 and Opus 5.5 reject forced `tool_choice`** (`any`/`tool`)
with a 400. Their presets (`claude_fable_5_1()`, `claude_opus_5_5()`) set
`AnthropicCompat::native_structured_output`, so `prompt_structured` works on
them. A hand-built `ModelConfig::anthropic("claude-opus-5-5", ..)` does not
set it and gets the 400 as `Provider { .. }` — use the preset or turn the
flag on:
```rust
let mut config = ModelConfig::anthropic("claude-opus-5-5", "Opus 5.5");
config.anthropic = Some(AnthropicCompat::default().with_native_structured_output(true));
```
Anthropic lists native JSON outputs for Fable 5/5.1, Opus 4.5 through 5.5,
Sonnet 4.5/4.6/5 and Haiku 4.5 (on Amazon Bedrock, fewer: see the
Anthropic structured-outputs page). The flag is off by default because
gateways that speak the Messages protocol may not accept
`output_config.format`.
- Markdown code fences around the JSON are stripped defensively before
parsing.