Structured Output
Generate type-safe Swift values directly from LLM responses using the @Generable macro.
Overview
Conduit's structured output system lets you define Swift types that models can populate directly. The @Generable macro synthesizes JSON schema, initializers, and partial types automatically. Combined with @Guide for property-level constraints, you get validated, typed responses from any provider.
The @Generable Macro
Apply @Generable to a struct or enum to opt it into structured generation:
@Generable
struct MovieReview {
let title: String
let rating: Int
let summary: String
let pros: [String]
let cons: [String]
}The macro synthesizes:
init(_ generatedContent: GeneratedContent)— Initializer from LLM outputstatic var generationSchema: GenerationSchema— JSON schema describing the typePartiallyGeneratednested type — For progressive streaming assemblygeneratedContentproperty — Convert back toGeneratedContent
Using with Providers
Pass the schema in the generation config:
let provider = AnthropicProvider(apiKey: "sk-ant-...")
let result = try await provider.generate(
messages: Messages {
Message.user("Review the movie Inception")
},
model: .claudeSonnet45,
config: .default.responseFormat(.jsonSchema(MovieReview.generationSchema))
)
let review = try MovieReview(GeneratedContent(json: result.text))
print(review.title) // "Inception"
print(review.rating) // 9The @Guide Macro
Add descriptions and constraints to individual properties:
@Generable
struct RecipeRequest {
@Guide("Name of the recipe")
let name: String
@Guide("Number of servings", .range(1...12))
let servings: Int
@Guide("Cuisine type", .anyOf(["Italian", "Mexican", "Japanese", "Indian", "French"]))
let cuisine: String
@Guide("Preparation time in minutes", .range(5...180))
let prepTime: Int
@Guide("List of ingredients needed")
let ingredients: [String]
}Guide Constraints
@Guide supports constraints via GenerationGuide:
String constraints:
.anyOf(["option1", "option2"])— Restrict to specific values.pattern("^[A-Z]")— Regex pattern matching.constant("fixed-value")— Exact value
Numeric constraints:
.range(1...100)— Inclusive range.minimum(0)— Lower bound.maximum(1000)— Upper bound
Array constraints:
.count(5)— Exact element count.minimumCount(1)— Minimum elements.maximumCount(10)— Maximum elements.element(guide)— Constraint on each element
Regex patterns:
@Generable
struct ContactInfo {
@Guide("Email address", /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)
let email: String
}Supported Types
@Generable works with:
- Structs with stored properties
- Enums with string raw values
- Nested @Generable types
- Optionals (
String?,Int?) - Arrays (
[String],[NestedType])
Property types that conform to ConvertibleFromGeneratedContent are supported:
String,Int,Double,Bool,Float[T]whereT: ConvertibleFromGeneratedContentT?whereT: ConvertibleFromGeneratedContent- Other
@Generabletypes
GenerationSchema
GenerationSchema describes the JSON schema for a type. It's auto-generated by @Generable but can be built manually:
// Auto-generated
let schema = MovieReview.generationSchema
// Convert to JSON for inspection
let json = schema.toJSONString()DynamicGenerationSchema
Build schemas at runtime for dynamic use cases:
let schema = DynamicGenerationSchema(
type: .object,
properties: [
"name": .init(type: .string, description: "User's name"),
"age": .init(type: .integer, description: "User's age"),
],
required: ["name", "age"]
)GeneratedContent
GeneratedContent is the intermediate representation between raw JSON and typed Swift values. It's a self-describing tree type:
// Parse from JSON
let content = try GeneratedContent(json: """
{"title": "Inception", "rating": 9}
""")
// Access values
let title: String = try content.value(forProperty: "title")
let rating: Int = try content.value(forProperty: "rating")
// Convert back to JSON
let json = content.jsonStringIncomplete JSON Recovery
During streaming, GeneratedContent can recover from incomplete JSON. This powers the PartiallyGenerated type for progressive UI updates:
// Even incomplete JSON can be partially parsed
let partial = try GeneratedContent(json: """
{"title": "Inception", "rating": 9, "summary": "A mind-bending
""")
// partial.isComplete == false
// But title and rating are still accessibleConvertibleFromGeneratedContent
The ConvertibleFromGeneratedContent protocol defines how types are initialized from GeneratedContent. Standard library types (String, Int, Double, Bool, arrays, optionals) already conform. Implement it for custom types:
struct Color: ConvertibleFromGeneratedContent {
let hex: String
init(_ content: GeneratedContent) throws {
self.hex = try content.value(forProperty: "hex")
}
}Enums with @Generable
Enums with string raw values work naturally:
@Generable
enum Sentiment: String {
case positive
case negative
case neutral
}
@Generable
struct Analysis {
@Guide("Overall sentiment of the text")
let sentiment: Sentiment
@Guide("Confidence score from 0 to 1")
let confidence: Double
}