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