Structured Output
Force the model to return a typed Swift struct instead of free-form text. Guaranteed to decode with no parsing and no guard chains.
How It Works
Under the hood, runStructured() registers a hidden tool whose input schema is your type's jsonSchema. The model must call that tool to respond and cannot return free text. The SDK then decodes the tool's input directly into your Swift type.
@StructuredOutput Macro
Apply @StructuredOutput to any Codable struct and the compiler synthesizes the jsonSchema property from the stored properties automatically:
@StructuredOutput
struct Recipe {
let name: String
let ingredients: [String]
let steps: [String]
let prepTimeMinutes: Int
let note: String? // optional (omitted from "required"
}
let agent = Agent(model: provider)
let recipe: Recipe = try await agent.runStructured("Give me a pasta recipe")
print(recipe.name) // "Spaghetti Carbonara"
print(recipe.ingredients[0]) // "200g spaghetti"
print(recipe.prepTimeMinutes) // 20
print(recipe.note ?? "") // may be nil
@StructuredOutput (attribute) and StructuredOutput (protocol) coexist, following the same pattern Swift Data uses with @Model/Model. Xcode may show a warning about the name overlap; it compiles and runs correctly.
Type Mapping
The macro maps stored properties to JSON schema types:
| Swift Type | JSON Schema | In required? |
|---|---|---|
String | {"type":"string"} | Yes |
Int, Int64, … | {"type":"integer"} | Yes |
Double, Float | {"type":"number"} | Yes |
Bool | {"type":"boolean"} | Yes |
[T] / Array<T> | {"type":"array","items":{...T's schema...}} | Yes |
T? / Optional<T> | Same as T | No |
| Other types | {"type":"object"} | Yes |
Array types
Arrays of primitives work automatically. Each element's schema is derived recursively:
@StructuredOutput
struct Report {
let title: String
let tags: [String] // → {"type":"array","items":{"type":"string"}}
let scores: [Double] // → {"type":"array","items":{"type":"number"}}
let flags: [Bool] // → {"type":"array","items":{"type":"boolean"}}
}
Optional fields
Optional properties are included in the schema but excluded from required, so the model can choose to omit them:
@StructuredOutput
struct ProductReview {
let productName: String // required
let rating: Int // required (1–5)
let summary: String // required
let pros: [String] // required
let cons: [String] // required
let verdict: String? // optional; model may skip
}
Manual Conformance
For nested types, polymorphic schemas, or any schema the macro cannot express, conform to StructuredOutput manually:
struct WeatherReport: StructuredOutput {
let city: String
let temperature: Double
let condition: String
let humidity: Int
static var jsonSchema: JSONSchema {
[
"type": "object",
"properties": [
"city": ["type": "string"],
"temperature": ["type": "number", "description": "Celsius"],
"condition": ["type": "string", "enum": ["sunny","cloudy","rainy","snowy"]],
"humidity": ["type": "integer", "minimum": 0, "maximum": 100],
],
"required": ["city", "temperature", "condition", "humidity"],
]
}
}
let report: WeatherReport = try await agent.runStructured("What's the weather in London?")
print("\(report.city): \(report.temperature)°C, \(report.condition)")
Use enum constraints in manual schemas to restrict the values the model can produce. This is especially useful for status fields, categories, and ratings.
Nested Types
The macro maps unknown types to {"type":"object"} without a nested schema. For structs with nested types, define jsonSchema manually to give the model the full schema:
// Nested type: define manually so the schema is complete
struct OrderSummary: StructuredOutput {
struct LineItem: Codable {
let product: String
let quantity: Int
let price: Double
}
let orderId: String
let items: [LineItem]
let total: Double
static var jsonSchema: JSONSchema {
[
"type": "object",
"properties": [
"orderId": ["type": "string"],
"items": [
"type": "array",
"items": [
"type": "object",
"properties": [
"product": ["type": "string"],
"quantity": ["type": "integer"],
"price": ["type": "number"],
],
"required": ["product", "quantity", "price"],
]
],
"total": ["type": "number"],
],
"required": ["orderId", "items", "total"],
]
}
}