Skip to content

Structured output

Ask for a value, get a value.

import Loopl

struct Weather: Codable, Sendable {
    let city: String
    let celsius: Int
    let summary: String?
}

func report(_ agent: Agent) async throws -> Weather {
    try await agent.structured(As: Weather.self, prompt: "Weather in Istanbul right now")
}

The JSON schema is derived from Decodable and offered to the model as one tool, structured_output. Its arguments are the answer; they decode straight into Weather and the run ends there.

If the model answers in prose, a second pass offers only that tool — on-device models have no tool_choice, so this is how loopl insists. Still prose: StructuredOutputError.notCalled, never a guess. Malformed arguments go back as an error naming the field, so the model can repair them (maxRepairs, default 1).

Options

import Loopl

struct Reading: Codable, Sendable { let city: String; let celsius: Int }

func tuned(_ model: any Model) async throws {
    let agent = Agent(model: model, tools: [CalculatorTool()])
    var options = StructuredOutputOptions()
    options.describe = ["city": "City name, as the user wrote it", "celsius": "Temperature in °C"]
    options.withTools = true          // the first pass may still use other tools
    options.maxRepairs = 2
    options.discardHistory = true     // leave the conversation untouched
    let run = try await agent.structuredRun(As: Reading.self, prompt: "weather in Izmir", options: options)
    print(run.value.city, run.forced, run.repairs, run.result.metrics.cycleCount)
}

Schemas

Swift JSON schema
String · Int · Double · Bool string · integer · number · boolean
T? not required
nested struct · [T] object · array
Date · URL · UUID · Data string with a format
enum E: String, Codable, SchemaEnum string + enum

A type that validates in init(from:), or a recursive one, can't be derived — SchemaDerivationError asks you to declare it:

import Loopl

struct Rating: Codable, Sendable, JSONSchemaProviding {
    let stars: Int
    static var jsonSchema: JSONValue {
        ToolSchema.object(["stars": ToolSchema.number("1 to 5")], required: ["stars"])
    }
}

Works on MLX and llama.cpp; best effort on Apple's model. Small models do better with few, well-described fields.