Foundation Models: Structured Output with @Generable and Constrained Decoding


You have a working LanguageModelSession, you can stream text from the on-device model, and your prompts are solid. Then the product requirement lands: “Parse the model’s response into a structured object we can persist to SwiftData.” Suddenly, free-form text is not enough. You need the model to output a specific shape of data --- every time, without post-processing regex hacks.

This is the problem @Generable and constrained decoding were built to solve. In this post you will learn how to define structured output schemas with @Generable, constrain the model’s generation with @Guide, and handle the edge cases that surface in production. We will not cover tool calling or agentic patterns --- those are their own topic in Tool Calling with Foundation Models.

Note: @Generable and constrained decoding require iOS 26+ and macOS 26+. All code in this post requires @available(iOS 26, *) annotations or an appropriate deployment target. This post assumes familiarity with the Foundation Models framework basics covered in Apple’s Foundation Models Framework and the custom Codable patterns discussed in Mastering Codable.

Contents

The Problem

Imagine you are building a Pixar movie catalog app. A user types a natural-language query --- “Tell me about that rat cooking movie from 2007” --- and you want to extract structured movie metadata from the on-device model’s response. Without structured output, you get back free-form text and have to parse it yourself.

import FoundationModels

@available(iOS 26, *)
func extractMovieInfo() async throws {
    let session = LanguageModelSession()

    let response = try await session.respond(
        to: "Extract the movie title, release year, and director from: " +
            "'That rat cooking movie from 2007'"
    )

    // response.content is a plain String:
    // "The movie is Ratatouille, released in 2007, directed by Brad Bird."
}

That string could arrive in dozens of different formats. You would need fragile regex or NLP post-processing to pull out the three fields. If the model rephrases slightly --- “Brad Bird directed Ratatouille (2007)” --- your parser breaks.

The root issue: you are asking a language model for structured data but accepting unstructured text. Constrained decoding flips this. Instead of parsing after generation, you tell the model the exact schema it must conform to during generation. Every token the model emits is validated against your type, so the output is guaranteed to decode.

Introducing @Generable

The @Generable macro marks a Swift type as a valid output schema for the Foundation Models framework. At compile time, it synthesizes the schema description the model uses to constrain its token generation.

Here is the same movie extraction, done properly with @Generable.

import FoundationModels

@available(iOS 26, *)
@Generable
struct MovieInfo {
    @Guide(description: "The official title of the Pixar or Disney movie")
    var title: String

    @Guide(description: "The four-digit release year")
    var releaseYear: Int

    @Guide(description: "The primary director's full name")
    var director: String
}

With the schema defined, you call respond with an explicit return type.

@available(iOS 26, *)
func extractStructuredMovieInfo() async throws {
    let session = LanguageModelSession()

    let result: MovieInfo = try await session.respond(
        to: "Extract the movie title, release year, and director from: " +
            "'That rat cooking movie from 2007'",
        generating: MovieInfo.self
    )

    print(result.title)       // Ratatouille
    print(result.releaseYear) // 2007
    print(result.director)    // Brad Bird
}

No parsing. No regex. The result is a fully typed MovieInfo instance. If the model cannot produce valid output for the schema, the framework throws an error rather than returning garbage.

How Constrained Decoding Works Under the Hood

When you pass generating: MovieInfo.self, the framework does not simply append “respond in JSON” to your prompt. It uses the synthesized schema to build a constrained decoding grammar. At each token position, the model’s logits are masked so that only tokens consistent with the schema’s current state are considered. This means:

  • String fields receive string tokens.
  • Numeric fields receive numeric tokens.
  • Enum fields are restricted to their declared cases.

The result is structurally valid by construction, not by luck.

Apple Docs: @Generable --- Foundation Models Framework

Constraining Output with @Guide

The @Generable macro gives you type safety, but @Guide gives you semantic constraints. Without @Guide, a String property can contain anything the model wants to say. With @Guide, you restrict it.

Descriptions

The most common use of @Guide is providing a natural-language description that tells the model what a property represents. This is not a comment for developers --- it is part of the schema the model sees during generation.

@available(iOS 26, *)
@Generable
struct CharacterProfile {
    @Guide(description: "The character's name as it appears in the movie credits")
    var name: String

    @Guide(description: "A one-sentence personality description")
    var personality: String

    @Guide(description: "The movie this character first appeared in")
    var debutMovie: String
}

Good descriptions are specific and unambiguous. “The character’s name as it appears in the movie credits” is better than “name” because it tells the model exactly which name you want --- not a nickname, not a description, the credited name.

Ranges on Numeric Types

For Int and Double properties, @Guide supports range constraints. The model physically cannot output a number outside the range.

@available(iOS 26, *)
@Generable
struct MovieRating {
    @Guide(description: "The movie title")
    var title: String

    @Guide(description: "Rating from 1 to 10", .range(1...10))
    var score: Int

    @Guide(description: "Estimated runtime in minutes", .range(60...240))
    var runtimeMinutes: Int
}

This is not validation after the fact. The constrained decoder masks logits for tokens that would produce an out-of-range number. A score of 11 is impossible, not just rejected.

Count Constraints on Collections

When a property is an Array, you can constrain its length.

@available(iOS 26, *)
@Generable
struct MovieCast {
    @Guide(description: "The movie title")
    var title: String

    @Guide(description: "The top three billed actors", .count(3))
    var leadActors: [String]

    @Guide(
        description: "Up to five notable supporting characters",
        .count(1...5)
    )
    var supportingCharacters: [String]
}

.count(3) means exactly three elements. .count(1...5) means between one and five. The model structures its output to respect these bounds.

Regex Patterns on Strings

For string properties that must match a specific format, @Guide accepts regex patterns.

@available(iOS 26, *)
@Generable
struct StudioRelease {
    @Guide(description: "The movie title")
    var title: String

    @Guide(
        description: "Release date in YYYY-MM-DD format",
        .pattern(#/\d{4}-\d{2}-\d{2}/#)
    )
    var releaseDate: String

    @Guide(
        description: "MPAA rating code",
        .pattern(#/G|PG|PG-13|R/#)
    )
    var mpaaRating: String
}

The regex pattern is compiled into the constrained decoding grammar. Every character the model generates for releaseDate must advance through the regex. A date like “2007-06-29” passes; “June 29, 2007” is structurally impossible.

Tip: Keep regex patterns simple. Complex patterns increase the grammar size and can slow down generation. If you need rich validation beyond what regex provides, consider using an enum instead.

Nested and Composed Schemas

Real-world data is rarely flat. @Generable supports nested types, optionals, arrays of generable types, and enums.

Enums

Enums with String raw values work naturally with @Generable. The model is constrained to output only the declared cases.

@available(iOS 26, *)
@Generable
enum Genre: String {
    case animation
    case adventure
    case comedy
    case drama
    case sciFi = "sci-fi"
    case fantasy
}

Nested Generable Types

You can compose @Generable types by nesting them.

@available(iOS 26, *)
@Generable
struct Director {
    @Guide(description: "Full name")
    var name: String

    @Guide(
        description: "Notable Pixar movies directed",
        .count(1...5)
    )
    var notableMovies: [String]
}

@available(iOS 26, *)
@Generable
struct PixarFilm {
    @Guide(description: "The movie's official title")
    var title: String

    @Guide(
        description: "Four-digit release year",
        .range(1995...2030)
    )
    var year: Int

    var genre: Genre

    var director: Director  // Nested @Generable type

    @Guide(description: "Box office gross in millions USD")
    var boxOfficeMillions: Double?  // Optional --- model may omit
}

When the model generates a PixarFilm, it descends into the Director schema for the director property, constrained at every level. The optional boxOfficeMillions can be present or absent --- the model decides based on available information.

Arrays of Generable Types

Combining arrays with nested schemas lets you extract collections of structured objects in a single call.

@available(iOS 26, *)
@Generable
struct FilmCatalogEntry {
    @Guide(description: "The movie's official title")
    var title: String

    @Guide(
        description: "Four-digit release year",
        .range(1995...2030)
    )
    var year: Int

    var genre: Genre
}

@available(iOS 26, *)
@Generable
struct CatalogResponse {
    @Guide(
        description: "List of Pixar films matching the query",
        .count(1...10)
    )
    var films: [FilmCatalogEntry]
}

@available(iOS 26, *)
func fetchFilmCatalog() async throws {
    let session = LanguageModelSession()

    let catalog: CatalogResponse = try await session.respond(
        to: "List all Pixar films released between 2015 and 2020",
        generating: CatalogResponse.self
    )

    for film in catalog.films {
        print("\(film.title) (\(film.year)) - \(film.genre)")
    }
}
Inside Out (2015) - comedy
The Good Dinosaur (2015) - adventure
Finding Dory (2016) - adventure
Cars 3 (2017) - animation
Coco (2017) - animation
Incredibles 2 (2018) - adventure
Toy Story 4 (2019) - animation
Onward (2020) - fantasy
Soul (2020) - drama

This pattern is powerful for extraction tasks: give the model a block of text or a user query, and get back a typed array of domain objects.

Advanced Usage

Streaming Structured Output

For large schemas or slow connections, you may want to stream partial results. LanguageModelSession supports this through respondStreaming.

@available(iOS 26, *)
func streamCharacterList() async throws {
    let session = LanguageModelSession()

    let stream = session.respondStreaming(
        to: "Describe the three main characters from Toy Story",
        generating: CharacterListResponse.self
    )

    for try await partial in stream {
        // partial.content is a partially-filled CharacterListResponse
        // Properties may be nil or incomplete until the stream completes
        if let characters = partial.content?.characters {
            print("Received \(characters.count) characters so far...")
        }
    }

    // After the stream completes, access the final result
    let finalResult = try await stream.finalResponse
    print(finalResult.content) // Fully populated CharacterListResponse
}

During streaming, the partial object’s properties populate incrementally. Strings may be truncated mid-word, arrays may have fewer elements than the final count, and optional properties may still be nil. Only the finalResponse is guaranteed to be complete and schema-valid.

Warning: Do not persist or make business logic decisions based on partial streaming content. Wait for finalResponse for any operation that requires complete data.

Combining @Generable with System Prompts

System prompts set the model’s behavior, while @Generable constrains its output structure. They complement each other.

@available(iOS 26, *)
func generateMovieReview() async throws {
    let session = LanguageModelSession(
        instructions: """
        You are a Pixar film expert. When asked about movies, provide
        accurate information based on officially released Pixar films
        only. Do not include non-Pixar Disney Animation films. If you
        are unsure about a detail, omit the optional field rather than
        guessing.
        """
    )

    let review: MovieReview = try await session.respond(
        to: "What did critics think of WALL-E?",
        generating: MovieReview.self
    )
}

The system prompt guides the model’s knowledge and tone; @Generable ensures the output fits your type. This separation of concerns keeps your schemas reusable across different prompt contexts.

Handling Generation Failures

Constrained decoding does not guarantee the model has the knowledge to fill your schema correctly --- it guarantees the structure is valid. A model might output "Unknown" for a string field or 0 for a numeric field when it lacks information.

@available(iOS 26, *)
func lookUpUnknownFilm() async {
    let session = LanguageModelSession()

    do {
        let film: PixarFilm = try await session.respond(
            to: "Tell me about the Pixar movie 'The Starlight Express'",
            generating: PixarFilm.self
        )
        // The model may fabricate data for a nonexistent movie
        // Structural validity does not imply factual accuracy
        print(film.title)
    } catch {
        // The model could not produce structurally valid output
        print("Generation failed: \(error.localizedDescription)")
    }
}

Tip: Use optional properties for fields the model might not know. Check for sentinel values (empty strings, zeros) in your domain logic. Constrained decoding solves the shape problem, not the accuracy problem.

Supported Property Types

Not every Swift type works with @Generable. The supported types are:

TypeNotes
StringSupports @Guide with .pattern()
IntSupports @Guide with .range()
DoubleSupports @Guide with .range()
BoolNo additional @Guide options
Optional<T>Where T is any supported type
[T]Supports .count(). T must be supported
enumString raw value. Cases become the set
Nested @Generable structFull recursive schema support

Notably absent: Date (use a String with a .pattern constraint), UUID (same approach), and any class or actor type. The macro requires value semantics.

Performance Considerations

Constrained decoding adds overhead compared to free-form text generation. The grammar constructed from your schema must be evaluated at every token position. Here is what affects performance in practice.

Schema complexity scales generation time. A flat struct with three String properties generates quickly. A nested struct with arrays of generable types, regex-constrained strings, and range-bounded numbers takes longer. Each constraint adds branches to the decoding grammar.

Array count constraints have the largest impact. An unconstrained [String] lets the model decide when to stop. A .count(1...50) constraint forces the grammar to track how many elements have been emitted. Keep array bounds as tight as your domain allows.

Regex patterns compile to finite automata. Simple patterns like \d{4}-\d{2}-\d{2} are cheap. Complex patterns with alternation, nested groups, and quantifiers create larger automata. If a regex-constrained property is slowing things down, consider replacing it with an enum or simplifying the pattern.

As a rough guideline for on-device generation:

Schema ShapeTypical Latency
Flat struct, 3—5 propertiesLow
Nested struct, 1 level deepLow—Medium
Array of generable types (5—10 items)Medium
Complex regex + nested arraysMedium—High

Tip: Profile with Instruments using the os_signpost integration in Foundation Models. Look for time spent in the constrained decoding phase versus raw token generation. If constrained decoding dominates, simplify your schema.

Apple Docs: LanguageModelSession --- Foundation Models Framework

When to Use (and When Not To)

@Generable is not the right tool for every interaction with the on-device model. Here is a decision framework.

ScenarioRecommendation
Extracting structured fields from user inputUse @Generable. Primary use case.
Generating UI-ready display textSkip @Generable. Free-form text is what you want.
Classification into known categoriesUse @Generable with an enum. Constrained to valid cases.
Populating a form with AI suggestionsUse @Generable. Map properties to form fields.
Chatbot conversation responsesSkip @Generable. Structured output fights conversation.
Extracting data to persist in SwiftDataUse @Generable. Map output to your model layer.
Generating lists of recommendationsUse @Generable with .count() for predictable sizes.
Agentic workflows with external APIsUse the Tool protocol.

The key question: do you need the model’s output in a specific shape? If yes, use @Generable. If you are displaying the model’s text directly to the user, free-form generation is simpler and faster.

One trade-off to keep in mind: constrained decoding can make the model’s output feel less natural. A free-form response might say “Ratatouille was directed by Brad Bird and released in 2007.” A @Generable response gives you { title: "Ratatouille", year: 2007, director: "Brad Bird" }. The structured version is machine-friendly but loses the narrative. For user-facing summaries, consider generating free-form text alongside structured data by making two calls, or include a freeform summary string property in your schema.

Summary

  • @Generable marks a Swift struct as a valid output schema for the Foundation Models framework. Constrained decoding guarantees the model’s output matches your type at every token.
  • @Guide adds semantic constraints: descriptions guide the model’s understanding, .range() bounds numbers, .count() controls array lengths, and .pattern() enforces string formats via regex.
  • Nested types, enums, optionals, and arrays compose naturally. Design your schemas like you design your domain models.
  • Structural validity does not imply factual accuracy. Use optional properties and domain-level validation for fields the model might not know.
  • Keep schemas as simple as your domain allows. Every constraint adds grammar overhead to the decoding step.

When you are ready to let the model call external functions based on structured arguments, head to Tool Calling with Foundation Models to build on everything covered here.