Structured Generation with Schemas

You use Foundation Models’ structured generation to produce properly typed Swift objects directly from the model instead of parsing unstructured text. You define the data structure using macros, and the framework guarantees type-safe results by constraining output to your schema during token generation.

Early work with LLMs often required brittle parsing of free-form text. Structured generation eliminates that overhead and improves reliability in production code.

Prerequisites and Context

This chapter builds on the streaming concepts from the streaming chapter and the session patterns from the sessions chapter. You should understand how to create sessions, control generation parameters, and work with streaming responses. The snapshot streaming patterns from the streaming chapter become more powerful with structured generation, allowing you to watch complex Swift objects populate field by field.

What You Will Learn

By the end of this chapter, you will be able to:

  • Transform unstructured AI responses into type-safe Swift objects using @Generable
  • Guide content generation with @Guide attributes for precise control
  • Build complex nested structures for rich data models
  • Handle optional fields and collections effectively
  • Stream structured content with real-time field population
  • Apply structured generation to practical scenarios like journaling and content analysis

Traditional AI Responses

With normal AI responses, you get unstructured text that requires manual parsing:

// Traditional approach: Parse unstructured text
let response = try await session.respond(to: "Recommend a book about MusicKit")
// Response: "I recommend 'Exploring MusicKit' by Rudrank Riyam. It is the best guide available on the internet..."
// Now manually extract title, author, description...

Foundation Models provides type-safe results to avoid this parsing step using constrained decoding. Constrained decoding guides the model’s output to meet specific structural requirements, matching the given schema. Here is a summarized breakdown:

  • Input and Constraints: The process begins with an input prompt or query that includes the desired structure or constraints.

By default, the methods in the framework includes the schema of the structure as part of the input in a specific format that the model has been trained on.

  • Decoding Process: The model processes the input while considering these constraints. During decoding, the model generates potential outputs that follow these constraints while masking tokens that are not valid.

  • Output Formatting: Once the model generates a draft response, it involves additional steps to format it according to the specified structure.

  • Validation and Correction: The output is validated against the constraints to ensure it meets all requirements.

Understanding the @Generable Macro

The @Generable macro is the foundation of structured generation in Foundation Models. This Swift macro transforms your data structures into compatible schemas while maintaining type safety.

When you apply @Generable to a structure or enumeration, the macro automatically:

  • Generates conformance to the Generable protocol
  • Creates initialization methods from AI-generated content
  • Produces JSON schemas that guide the AI model’s output format
  • Handles type conversion between AI responses and Swift types

The macro supports both structures and enumerations:

@Generable struct RecipeRecommendation {
    @Guide(description: "The name of the dish")
    let name: String
    
    @Guide(description: "Brief cooking instructions")
    let instructions: String
    
    @Guide(description: "The cuisine type this recipe belongs to")
    let cuisine: CuisineType
}

@Generable
enum CuisineType {
    case italian
    case mexican
    case asian
    case mediterranean
    case indian
}

Understanding the @Guide Macro

The @Guide macro fine-tunes how the model generates content for specific properties of your @Generable types. It provides three ways to influence the output generation:

Description-Based Guidance

The most common use provides descriptive text to guide the model:

@Generable
struct RecipeDetails {
    @Guide(description: "A clear recipe name, maximum 60 characters")
    let name: String
    
    @Guide(description: "Cooking time in minutes, between 5 and 240")
    let cookingTime: Int
    
    @Guide(description: "Step-by-step cooking instructions")
    let instructions: String
    
    @Guide(description: "The cuisine type this recipe belongs to")
    let cuisine: CuisineType
}

Here is an example of using the RecipeDetails structure to generate a recipe:

#Playground {
    let session = LanguageModelSession()
    let recipe = try await session.respond(
        to: "Butter chicken biryani",
        generating: RecipeDetails.self
    )
}

The model returns a fully structured RecipeDetails object:

recipe: RecipeDetails
    name: "Butter Chicken Biryani"
    cookingTime: 60
    instructions: "1. Soak basmati rice in water for 30 minutes. 2. In a separate pan, cook chicken pieces with butter, cream, and spices until tender. 3. Layer rice and chicken in a biryani pot. 4. Repeat layers and top with fried onions and nuts. 5. Cook on low heat until rice is fully cooked."
    cuisine: CuisineType.indian

Now the unstructured example with Foundation Models becomes:

@Generable
struct BookRecommendation {
    @Guide(description: "The book title")
    let title: String

    @Guide(description: "Author name")
    let author: String

    @Guide(description: "Brief description")
    let description: String
}

// Get structured result
let book = try await session.respond(
    to: "Recommend a book about MusicKit",
    generating: BookRecommendation.self
)

// Use immediately in Swift code
print("Book: (book.title) by (book.author)")
// Book: "Exploring MusicKit" by Rudrank Riyam

Regex Pattern Matching

For string properties requiring specific formats, use regex patterns:

@Generable
struct RecipeMetadata {
    @Guide(description: "Recipe difficulty level", /^(Easy|Medium|Hard)$/)
    let difficulty: String
    
    @Guide(description: "Prep time in HH:MM format", /^d{2}:d{2}$/)
    let prepTime: String
}

The @Guide macro is there to ensure the model generates content that matches your specified patterns, improving the output of structured generation.

Nested Structures

You can use nested @Generable types for complex data relationships:

@Generable
struct StoryOutline {
    @Guide(description: "Compelling story title")
    let title: String

    @Guide(description: "Main character details")
    let protagonist: Character

    @Guide(description: "Story setting")
    let setting: Setting

    @Guide(description: "Major plot points")
    let plotPoints: [PlotPoint]
}

@Generable
struct Character {
    @Guide(description: "Character name")
    let name: String

    @Guide(description: "Character motivation")
    let motivation: String
}

@Generable
struct Setting {
    @Guide(description: "Time period")
    let timePeriod: String

    @Guide(description: "Primary location")
    let location: String
}

@Generable
struct PlotPoint {
    @Guide(description: "Brief description of the plot event")
    let description: String
    
    @Guide(description: "Order in the story: beginning, middle, end")
    let position: StoryPosition
}

@Generable
enum StoryPosition: String, CaseIterable {
    case beginning, middle, end
}

You can then generate a complete story outline with nested details:

#Playground {
    let session = LanguageModelSession(instructions: "You are a creative writing assistant.")
    let outline = try await session.respond(
    to: "Create a story outline about a time-traveling detective",
        generating: StoryOutline.self
    ).content

    print(outline)
}

Here is the output:

StoryOutline(
    title: "Echoes of Time: The Detective's Journey", 
    protagonist: Character(
        name: "Detective Elara Quinn",
        motivation: "Seeking closure for her late brother, whose mysterious death remains unsolved"
        ), 
    setting: Setting(
        timePeriod: "21st Century", 
        location: "Modern-day New York City"
        ), 
    plotPoints: [
        PlotPoint(
            description: "Detective Elara Quinn discovers an experimental time-travel device in her late brother's study.", 
            position: .beginning), 
        PlotPoint(
            description: "Using the device, Elara travels to the 1920s to investigate a series of unsolved murders in New York City.", 
            position: .middle), 
        PlotPoint(
            description: "In the past, Elara encounters a rival detective who seems eerily familiar and helps her uncover a hidden conspiracy.", 
            position: .middle), 
        PlotPoint(
            description: "Returning to the present, Elara realizes the rival detective from the past is somehow connected to her brother's death.",
            position: .middle), 
        PlotPoint(
            description: "Elara races against time to prevent a future catastrophe linked to the events she uncovered.",
            position: .end), 
        PlotPoint(
            description: "With the truth revealed, Elara finds peace and closure, understanding her brother's fate was never meant to be.",
            position: .end)
    ]
)

Optional Fields and Collections

Not all data is required. Foundation Models handles optional properties and dynamic arrays:

@Generable
struct Event {
    @Guide(description: "Event name")
    let title: String

    @Guide(description: "Event date in YYYY-MM-DD format")
    let date: String

    @Guide(description: "Location (if specified)")
    let location: String?

    @Guide(description: "Attendee count (if known)")
    let attendeeCount: Int?
}

You can then generate an event:

#Playground {
    let session = LanguageModelSession(instructions: "You are a event assistant.")

    let event = try await session.respond(
    to: "Schedule a team meeting for tomorrow at 2 PM",
        generating: Event.self
    )

    print(event.content)
}

And here is the output:

Event(
    title: "Team Meeting", 
    date: "2025-08-27", 
    location: nil, 
    attendeeCount: nil
)

Advanced @Guide Features

The @Guide macro supports constraints for more control:

Count Requirements

You can also specify exact counts or ranges using the Guide macro. It uses constrained decoding to guarantee the exact count or a value in the given range:

@Generable
struct ProductReview {
    @Guide(description: "Product name")
    let name: String

    @Guide(description: "Exactly 3 key features", .count(3))
    let keyFeatures: [String]

    @Guide(description: "2-5 pros of the product")
    let pros: [String]

    @Guide(description: "Brief summary, 50-100 words")
    let summary: String
    
    @Guide(description: "Rating from 1 to 5 stars", .range(1...5))
    let rating: Int
}

Here is a practical example using the ProductReview with count and range constraints:

#Playground {
    let session = LanguageModelSession(
        instructions: "You are a helpful product review analyzer."
    )
    
    let review = try await session.respond(
        to: "Analyze this iPhone 16 Pro: Amazing camera system with 5x optical zoom, excellent build quality, and great performance. Battery life could be better for heavy users and it is quite expensive.",
        generating: ProductReview.self
    )
    
    print(review.content)
}

And here is the output:

ProductReview(
    name: "iPhone 16 Pro", 
    keyFeatures: [
        "Amazing camera system with 5x optical zoom", 
        "Excellent build quality", 
        "Great performance"
    ], 
    pros: [
        "Excellent camera system", 
        "Excellent build quality", 
        "Great performance"
    ], 
    summary: "The iPhone 16 Pro boasts a top-notch camera system, premium build, and stellar performance. However, it may not last long on a single charge for heavy users and comes at a steep price.", 
    rating: 4
)

Notice how the model respected the constraints:

  • Exactly 3 key features as specified by .count(3)
  • 3 pros (the model interpreted the “2-5 pros” instruction from the description)
  • Rating of 4, within the 1-5 numeric range specified by .range(1...5)

The .range() constraint with ranges like .range(2...5) will not work for the number of items in the array. For array length constraints, it is better to use clear descriptions like “2-5 pros of the product” and let the model interpret the instruction naturally.

Journaling and Structured Analysis

Foundation Models can also analyze personal writing and extract structured information. For example, you can use it to analyze a journal entry:

@Generable
struct JournalAnalysis {
    @Guide(description: "Emotional tone: happy, sad, excited, anxious, reflective, grateful, frustrated")
    let mood: Mood

    @Guide(description: "Key topics or themes mentioned in the entry")
    let topics: [String]

    @Guide(description: "One-sentence summary of the main point")
    let summary: String

    @Guide(description: "A thoughtful, personalized question that helps with self-reflection based on the specific journal entry content")
    let nextPrompt: String?

    @Guide(description: "Life areas mentioned: work, relationships, health, hobbies, goals")
    let lifeAreas: [LifeArea]
}

@Generable
enum Mood: String, CaseIterable {
    case happy, sad, excited, anxious, reflective, grateful, frustrated, neutral
}

@Generable
enum LifeArea: String, CaseIterable {
    case work, relationships, health, hobbies, goals, family, travel, learning
}

@Generable
struct Challenge {
    @Guide(description: "Description of the challenge or concern")
    let description: String
    
    @Guide(description: "Severity level from 1-10", .range(1...10))
    let severity: Int
}

@Generable
enum Timeline: String, CaseIterable {
    case shortTerm = "short-term"
    case mediumTerm = "medium-term" 
    case longTerm = "long-term"
}

Journaling Example

Here is how a journaling app like Day One might use this:

#Playground {
    let session = LanguageModelSession(
        instructions: "You are a thoughtful journaling assistant. Help users reflect on their experiences."
    )
    
    let journalAnalysis = try await session.respond(
        to: """
    Today is my last day in Chiang Mai. I wanted to go shopping for some muay thai shorts but there is a storm outside so I am stuck in a cafe working on this book instead. I hope the storm weakens because I have a flight to Bangkok later tonight and I cannot miss it otherwise I will miss my subsequent flights to India. I know it is not in my control but it worries me. Working on this book is a good distraction.
    """,
        generating: JournalAnalysis.self
    ).content
    
    print(journalAnalysis)
}

And here is the output:

JournalAnalysis(
    mood: .anxious, 
    topics: ["shopping plans disrupted", "flight concerns", "working on a book as a distraction"], 
    summary: "The user is anxious about missing their flight to Bangkok due to a storm, which has disrupted their shopping plans in Chiang Mai.", 
    nextPrompt: "How can I manage my anxiety about missing this flight while staying present in the moment?", 
    lifeAreas: [.travel, .work]
)

Nested Structures for Detailed Analysis

For more sophisticated journaling apps, you can create nested structures to analyze the journal entry in more detail:

@Generable
struct DetailedJournalInsight {
    @Guide(description: "Overall emotional analysis")
    let emotional: EmotionalAnalysis
    
    @Guide(description: "Goals and aspirations mentioned")
    let goals: [Goal]
    
    @Guide(description: "Challenges or concerns raised")
    let challenges: [Challenge]
    
    @Guide(description: "Growth opportunities identified")
    let growthAreas: [String]
}

@Generable
struct EmotionalAnalysis {
    @Guide(description: "Primary emotion expressed")
    let primary: Mood
    
    @Guide(description: "Secondary emotions present")
    let secondary: [Mood]
    
    @Guide(description: "Emotional intensity from 1-10", .range(1...10))
    let intensity: Int
}

@Generable
struct Goal {
    @Guide(description: "The specific goal or aspiration")
    let description: String
    
    @Guide(description: "Timeline mentioned: short-term, medium-term, long-term")
    let timeline: Timeline
    
    @Guide(description: "Confidence level in achieving this goal from 1-10", .range(1...10))
    let confidence: Int
}

@Generable
struct Challenge {
    @Guide(description: "Description of the challenge or concern")
    let description: String
    
    @Guide(description: "Severity level from 1-10", .range(1...10))
    let severity: Int
}

@Generable
enum Timeline: String, CaseIterable {
    case shortTerm = "short-term"
    case mediumTerm = "medium-term" 
    case longTerm = "long-term"
}

Here is how to use the detailed analysis:

#Playground {
    let session = LanguageModelSession(
        instructions: "You are an expert journal analyst. Analyze emotions, goals, and challenges in detail."
    )
    
    let detailedInsight = try await session.respond(
        to: """
    Today is my last day in Chiang Mai. I wanted to go shopping for some muay thai shorts but there is a storm outside so I am stuck in a cafe working on this book instead. I hope the storm weakens because I have a flight to Bangkok later tonight and I cannot miss it otherwise I will miss my subsequent flights to India. I know it is not in my control but it worries me. Working on this book is a good distraction.
        """,
        generating: DetailedJournalInsight.self
    ).content
    
    print(detailedInsight)
}

And here is the output:

DetailedJournalInsight(
    emotional: EmotionalAnalysis(
        primary: .anxious, 
        secondary: [.neutral, .frustrated], 
        intensity: 7
    ), 
    goals: [
        Goal(
        description: "Shop for muay thai shorts", 
        timeline: .shortTerm, 
        confidence: 8
        ),
        Goal(
        description: "Complete the book", 
        timeline: .longTerm, 
        confidence: 6
        )
    ], 
    challenges: [
        Challenge(
            description: "Storm delaying flights", 
            severity: 8
        ), 
        Challenge(
            description: "Missed flights to Bangkok and India", 
            severity: 9
        )
    ], 
    growthAreas: [
        "Developing resilience in handling unexpected delays", 
        "Enhancing time management skills to meet travel deadlines"
    ]
)

Nutrition Data Parsing

One of the cool features in Zenther uses the new vision API that can analyze the table of a nutrition label and get the nutrition data in a structured format. I used @Generable types to eliminate the manual work of parsing nutrition data.

For voice-based food logging, I used @Generable types to parse natural language descriptions into structured nutrition data:

@available(iOS 26, *)
@Generable
struct NutritionParseResult: Codable {
    @Guide(description: "Clear name of the food item(s), e.g. 'Oatmeal with banana and almonds'")
    let foodName: String

    @Guide(description: "Total calories for the portion, rounded to whole numbers")
    let calories: Double

    @Guide(description: "Total protein in grams, rounded to one decimal place")
    let proteinGrams: Double

    @Guide(description: "Total carbohydrates in grams, rounded to one decimal place")
    let carbsGrams: Double

    @Guide(description: "Total fat in grams, rounded to one decimal place")
    let fatGrams: Double
}

And then get the transcription from the new speech analyzer API and parse it into the NutritionParseResult type:

func parseFood(_ description: String) async throws -> ParsedFood {
    let prompt = """
       Parse this food description into nutritional data: "(description)"
        
        Examples of good parsing:
        "I had 2 scrambled eggs with toast" → Consider: 2 large eggs (~140 cal), 1 slice toast (~80 cal), cooking butter (~30 cal)
        "protein shake after workout" → Consider: 1 scoop protein powder (~120 cal) + milk/water
        "pizza slice for lunch" → Consider: 1 slice medium pizza (~280 cal)
        "handful of almonds" → Consider: ~20 almonds (~160 cal)
        
        Be realistic about portions people actually eat.
        Account for cooking methods and common additions.
        """
    
    let response = try await nutritionSession.respond(
        to: prompt,
        generating: NutritionParseResult.self
    )

    return response.content
}

For scanning nutrition labels, I used more complex structured generation with precise calculation instructions because the labels use per-100g values and I had to also look into the values per serving:

@available(iOS 26.0, *)
@Generable
public struct NutritionLabelParsing: Codable {
    @Guide(description: "Name of the food product, or 'Nutrition Label' if no specific name found")
    let foodName: String    

    @Guide(description: "Serving size with unit, e.g. '1 cup', '30g', '1 slice'")
    let servingSize: String

    @Guide(description: "Calories per serving as a precise number (can include decimals)")
    let calories: Double

    @Guide(description: "Protein in grams per serving with full decimal precision (e.g., 7.8, 24.5, 12.3)")
    let proteinGrams: Double

    @Guide(description: "Total carbohydrates in grams per serving with full decimal precision (e.g., 45.2, 12.7)")
    let carbsGrams: Double

    @Guide(description: "Total fat in grams per serving with full decimal precision (e.g., 8.5, 15.3)")
    let fatGrams: Double

    @Guide(description: "Fiber in grams per serving with full decimal precision if available, otherwise 0")
    let fiberGrams: Double

    @Guide(description: "Total sugars in grams per serving with full decimal precision if available, otherwise 0")
    let sugarGrams: Double

    @Guide(description: "Sodium in milligrams per serving with full decimal precision if available, otherwise 0")
    let sodiumMg: Double
}

The system instructions include detailed parsing rules that ensure accurate nutrition label extraction:

public static let nutrition = """
You are a nutrition expert specializing in parsing nutrition labels and food data.

CRITICAL PARSING RULES:

STEP 1: IDENTIFY THE SERVING SIZE
- Look for "Serving size:", "Per serving:", or similar indicators
- This is your target serving size (e.g., "16.7g")

STEP 2: IDENTIFY ALL PER-100G VALUES
- Find ALL nutritional values in the per-100g column (ignore % RDA columns)
- Common nutrients: Energy/Calories, Protein, Carbohydrate, Total Fat, Saturated Fat, Total Sugars, Added Sugars, Fiber, Sodium, Cholesterol

STEP 3: CALCULATE EVERY VALUE USING SAME FORMULA
- For EVERY single nutrient, use: serving_value = (per_100g_value × serving_size) ÷ 100
- Do NOT use any value directly - ALWAYS calculate

MANDATORY CALCULATIONS (example for 16.7g serving):
- Calories: (per_100g_calories × 16.7) ÷ 100
- Protein: (per_100g_protein × 16.7) ÷ 100  
- Carbs: (per_100g_carbs × 16.7) ÷ 100
- Fat: (per_100g_fat × 16.7) ÷ 100
- Sugar: (per_100g_sugar × 16.7) ÷ 100
- Fiber: (per_100g_fiber × 16.7) ÷ 100
- Sodium: (per_100g_sodium × 16.7) ÷ 100

VERIFICATION CHECK:
- If serving is 16.7g, ALL values should be roughly 1/6th of the per-100g values
- If any value seems too high (like 38g sugar for 16.7g serving), you made an error

PRECISION:
- Round calories to nearest 0.1 kcal
- Round macros to nearest 0.1g
- Round sodium to nearest 1mg

OUTPUT FORMAT:
- Food name: Extract product name or use "Nutrition Label" if unclear
- Serving size: Use the actual serving size from the label (e.g., "16.7g")
- All nutrients: Use ONLY the calculated per-serving values (never use raw per-100g values)
"""

This approach did the trick to accurately extract nutrition data from nutrition labels but it was still a bit fragile and I had to add some error handling, especially because the model is not good with calculating values per serving.

Streaming with Structured Generation

Streaming works with @Generable types as well, allowing you to watch structured data populate in real-time. This is useful for complex responses like forms, summaries, or structured content:

@Generable
struct StoryOutline {
    @Guide(description: "Story title")
    let title: String

    @Guide(description: "Main character details")
    let protagonist: Character

    @Guide(description: "List of 3-5 plot points")
    let plotPoints: [String]
}

@Generable
struct Character {
    @Guide(description: "Character name")
    let name: String

    @Guide(description: "Character background")
    let background: String
}

func streamStoryOutline() async {
    do {
        let stream = session.streamResponse(
            to: "Create a story outline for a mystery novel",
            generating: StoryOutline.self
        )
        
        for try await snapshot in stream {
            await MainActor.run {
                // snapshot.content is StoryOutline.PartiallyGenerated
                // Fields populate as the model generates them
                updateStoryUI(with: snapshot.content)
            }
        }
        
        // Get complete result
        let finalOutline = try await stream.collect()
        await MainActor.run {
            displayFinalOutline(finalOutline.content)
        }
    } catch {
        handleStreamingError(error)
    }
}

What’s Next

If you have been working with AI responses in your apps, you know how frustrating it can be to parse unstructured text reliably. Structured generation with @Generable entirely eliminates that frustration. The type-safe Swift objects you get back are guaranteed to match your schema, and the streaming capabilities create genuinely responsive UIs.

In practice, start small. Pick one place where you are currently parsing AI text and convert it to use @Generable. Then expand to more complex nested structures as you get comfortable with the patterns.

With structured generation patterns established, the next chapter explores basic tool use for extending model capabilities beyond text generation. You will learn how to build tools that can access external data and perform actions, using the structured generation patterns you have learned here to create type-safe tool arguments and outputs.