Get data, not prose
When you need a value — not a paragraph — call generate() instead of
send(). It runs one constrained turn with no tools, validates the model's
JSON against your schema in-process, and returns the result. A structured
request never silently degrades to free text: it returns valid data or throws
a typed error.
Three ways to call it
The simplest form takes a ToolSchema and returns a validated JSONValue:
let schema = ToolSchema.object(
properties: [
"name": ToolSchemaProperty(schema: .string()),
"age": ToolSchemaProperty(schema: .integer),
],
required: ["name", "age"]
)
let value = try await agent.generate(from: "Describe Ada Lovelace.", schema: schema)
let name = value["name"]?.stringValue
Pair the schema with any Decodable to get a Swift value back:
struct PersonRecord: Decodable {
let name: String
let age: Int
}
let record = try await agent.generate(
from: "Describe Ada Lovelace.",
schema: schema,
as: PersonRecord.self
)
Or conform to StructuredOutput so the type carries its own schema — the
type is the contract, and the same call works identically across every
provider that supports it:
struct Person: StructuredOutput, Equatable {
let name: String
let age: Int
static var outputSchema: ToolSchema {
.object(
properties: [
"name": ToolSchemaProperty(schema: .string()),
"age": ToolSchemaProperty(schema: .integer),
],
required: ["name", "age"]
)
}
}
let person = try await agent.generate(from: "Describe Ada Lovelace.", as: Person.self)
On success, the turn appends your prompt and the model's raw JSON to
conversation, so the exchange survives as history.
Validation is always strict
Every generate() overload validates the model's output against the schema
inside the SDK, on every provider. The optional strict: parameter is a
provider-side enforcement hint only (it maps to native constrained-decoding
modes where they exist) — passing strict: false never relaxes the SDK's own
validation.
Optional null means absent
Some providers emit every schema property and use null for the optional
ones. The SDK normalizes this: a non-required property returned as null is
treated as if it were omitted.
struct Profile: Decodable, Equatable {
let name: String
let nickname: String?
}
let profileSchema = ToolSchema.object(
properties: [
"name": ToolSchemaProperty(schema: .string()),
"nickname": ToolSchemaProperty(schema: .string()),
],
required: ["name"]
)
If the model returns {"name": "Ada", "nickname": null}, the typed overloads
decode nickname as nil, and the JSONValue overload drops the key. A
required property returned as null still fails validation.
When it fails
Every failure throws a StructuredOutputError:
| Case | Meaning |
|---|---|
unsupported |
the provider does not support structured output — thrown before any request is sent |
unexpectedToolCall(event:) |
the provider emitted a tool event during a structured turn |
notJSON(rawPreview:) |
the output was not parseable JSON (the preview is truncated) |
schemaValidationFailed([SchemaViolation]) |
valid JSON that violates the schema — each violation carries a path and message |
decodeFailed(String) |
schema-valid JSON that does not decode into your type — a type/schema mismatch on your side |
A failed structured turn appends no assistant message. The prompt you sent
stays in history (except for unsupported, which throws before anything is
appended), so a retry sees the same conversation. Cancelling a structured
turn — agent.cancel() or cancelling the task — throws and never returns a
value, even if a complete payload had already streamed.
Provider support
| Provider | Structured output |
|---|---|
| OpenAI | native (strict JSON schema mode) |
| Gemini | native (response schema) |
| Apple on-device | native (guided generation) |
| Anthropic | not supported — generate() throws unsupported before any request |
| Your backend | opt-in — declare supportsStructuredOutput and the SDK sends the schema on the wire, then validates the reply |
| AgentKit Cloud | not supported — throws unsupported before any request |
The gate runs before the provider is touched and before anything is appended to the conversation, so an unsupported call leaves the session exactly as it was.
Next
- Capability matrix — every capability, every provider.
- Error reference — the full typed error surface.