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 emulated via forced tool-use: the SDK forces a synthetic tool whose input schema is your response schema, then validates the tool input
Your backend opt-in — declare supportsStructuredOutput and the SDK sends the schema on the wire, then validates the reply
AgentKit Cloud per tier — supported on the OpenAI, Gemini, and Anthropic-backed tiers; the cloud advertises it in its handshake. Seed it or let the first turn adopt it, then the SDK sends a portable schema and validates the reply

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.

One ToolSchema is portable across every backend: the same object schema you write here drives OpenAI, Gemini, Anthropic, the on-device model, and the cloud. Each backend enforces its own limits and fails typed at the call site if a schema exceeds them — the cloud constrains schemas to a portable subset (an object root, every property listed and made nullable for optionals, string-only enums, bounded nesting) so one schema works on whichever model your tier resolves to. See AgentKit Cloud for the per-tier contract.

Next