How to generate dynamic data structures with Apple Foundation Models
Over the past few days, I got really hung up in my attempts generate data structures using Apple Foundation Models for which the exact shape of that data wasn't known until runtime. The new APIs actually provide for this capability via DynamicGenerationSchema, but the WWDC sessions and sample code were too simple to follow this thread end-to-end:
- Start with a struct representing a
PromptSet
: a variable set of prompts that will either map onto or be used to define the ultimate response data structure 🔽 - Instantiate a
PromptSet
with—what else?—a set of prompts to get the model to generate the sort of data we want 🔽 - Build out a
DynamicGenerationSchema
based on the contents of a givenPromptSet
instance 🔽 - Create a struct that can accommodate the variably-shaped data with as much type safety as possible and which conforms to ConvertibleFromGeneratedContent, so it can be instantiated by passing a LanguageModelSession response's GeneratedContent 🔽
- Pull it all together and generate some data with the on-device foundation models! 🔽
Well, it took me all morning to get this to work, but I did it. Since I couldn't find a single code example that did anything like this, I figured I'd share this write up. You can read the code as a standalone Swift file or otherwise follow along below.
1. Define a PromptSet
Start with whatever code you need to represent the set(s) of prompts you'll be dealing with at runtime. (Maybe they're defined by you and ship with your app, maybe you let users define them through your app's UI.) To keep things minimal, I defined this one with a couple of mandatory fields and a variable number of custom ones:
struct EducationalPromptSet {
let type: String
let instructions: String
let name: String
let description: String
let summaryGuideDescription: String
let confidenceGuideDescription: String
let subComponents: [SubComponentPromptSet]
}
struct SubComponentPromptSet {
let title: String
let bodyGuideDescription: String
}
Note that rather than modeling the data itself, the purpose of these structs is to model the set of prompts that will ultimately drive the creation of the schema which will, in turn, determine the shape and contents of the data we get back from the Foundation Models API. To drive this home, whatever goes in summaryGuideDescription
, confidenceGuideDescription
, and bodyGuideDescription
should themselves be prompts to guide the generation of like-named type-safe values.
Yes, it is very meta.
2. Instantiate our PromptSet
Presumably, we could decode some JSON from a file or received over the network that could populate this EducationalPromptSet
. Here's an example set of prompts for generating cocktail recipes, expressed in some sample code:
let cocktailPromptSet = EducationalPromptSet(
type: "bartender_basic",
instructions: """
You are an expert bartender. Take the provided cocktail name or list of ingredients and explain how to make a delicious cocktail. Be creative!
""",
name: "Cocktail Recipe",
description: "A custom cocktail recipe, tailored to the user's input and communicated in an educational tone and spirit",
summaryGuideDescription: "The summary should describe the history (if applicable) and taste profile of the cocktail",
confidenceGuideDescription: "Range between 0-100 for your confidence in the feasibility of this cocktail based on the prompt",
subComponents: [
SubComponentPromptSet(title: "Ingredients", bodyGuideDescription: "A list of all ingredients in the cocktail"),
SubComponentPromptSet(title: "Steps", bodyGuideDescription: "A list of the steps to make the cocktail"),
SubComponentPromptSet(title: "Prep", bodyGuideDescription: "The bar prep you should have completed in advance of service")
]
)
You can see that the provided instruction, description, and each guide description really go a long way to specify what kind of data we are ultimately looking for here. This same format could just as well be used to specify an EducationalPromptSet
for calculus formulas, Japanese idioms, or bomb-making instructions.
3. Build a DynamicGenerationSchema
Now, we must translate our prompt set into a DynamicGenerationSchema.
Why DynamicGenerationSchema
and not the much simpler and defined-at-compile-time GenerationSchema that's expanded with the @Generable? Because reasons:
- We only know the prompts (in API parlance, "Generation Guide descriptions") at runtime, and the @Guide macro must be specified statically
- We don't know how many
subComponents
a prompt set instance will specify in advance - While
subComponents
may ultimately redound to an array of strings, that doesn't mean they represent like concepts that could be generated by a single prompt (as an array of ingredient names might). Rather, each subComponent is effectively the answer to a different, unknowable-at-compile-time prompt of its own
As for building the DynamicGenerationSchema
, you can break this up into two roots and have the parent reference the child, but after experimenting, I preferred just specifying it all in one go. (One reason not to get too clever about extracting these is that DynamicGenerationSchema.Property is not Sendable, which can easily lead to concurrency-safety violations).
This looks like a lot because this API is verbose as fuck, forcing you to oscillate between nested schemas and properties and schemas:
let cocktailSchema = DynamicGenerationSchema(
name: cocktailPromptSet.name,
description: cocktailPromptSet.description,
properties: [
DynamicGenerationSchema.Property(
name: "summary",
description: cocktailPromptSet.summaryGuideDescription,
schema: DynamicGenerationSchema(type: String.self)
),
DynamicGenerationSchema.Property(
name: "confidence",
description: cocktailPromptSet.confidenceGuideDescription,
schema: DynamicGenerationSchema(type: Int.self)
),
DynamicGenerationSchema.Property(
name: "subComponents",
description: cocktailPromptSet.confidenceGuideDescription,
schema: DynamicGenerationSchema(
name: "subComponents",
properties: cocktailPromptSet.subComponents.map { subComponentPromptSet in
DynamicGenerationSchema.Property(
name: subComponentPromptSet.title,
description: subComponentPromptSet.bodyGuideDescription,
schema: DynamicGenerationSchema(type: String.self)
)
}
)
)
]
)
4. Define a result struct that conforms to ConvertibleFromGeneratedContent
When conforming to ConvertibleFromGeneratedContent, a type can be instantiated with nothing more than the GeneratedContent returned from a language model response.
There is a lot going on here. Code now, questions later:
struct EducationalResult : ConvertibleFromGeneratedContent {
let summary: String
let confidence: Int
let subComponents: [SubComponentResult]
init(_ content: GeneratedContent) throws {
summary = try content.value(String.self, forProperty: "summary")
confidence = try content.value(Int.self, forProperty: "confidence")
let subComponentsContent = try content.value(GeneratedContent.self, forProperty: "subComponents")
let properties: [String: GeneratedContent] = {
if case let .structure(properties, _) = subComponentsContent.kind {
return properties
}
return [:]
}()
subComponents = try properties.map { (title, bodyContent) in
try SubComponentResult(title: title, body: bodyContent.value(String.self))
}
}
}
struct SubComponentResult {
let title: String
let body: String
}
That init
constructor is doing the Lord's work, here, because Apple's documentation really fell down on the job this time. See, through OS 26 beta 4, if you had a GeneratedContent
, you could simply iterate over a dictionary of its properties
or an array of its elements
. These APIs, however, appear to have been removed in OS 26 beta 5. I say "appear to have been removed," because Apple shipped Xcode 26 beta 5 with outdated developer documentation that continues to suggest they should exist and which failed to include beta 5's newly-added GeneratedContent.Kind enum. Between this and the lack of any example code or blog posts, I spent most of today wondering whether I'd lost my goddamn mind.
Anyway, good news: you can iterate over a dynamic schema's collection of properties of unknown name and size by unwrapping the response.content.kind enumerator. In my case, I know my subComponents
will always be a structure, because I'm the guy who defined my schema and the nice thing about the Foundation Models API is that its responses always, yes, always adhere to the types specified by the requested schema, whether static or dynamic.
So let's break down what went into deriving the value's customProperties
property.
We start by fetching a nested GeneratedContent
from the top-level property named subComponents
with content.value(GeneratedContent.self, forProperty: "subComponents")
Next, this little nugget assigns to properties
a dictionary mapping String
keys to GeneratedContent
values by unwrapping the properties from the kind
enumerator's structure case, and defaulting to an empty dictionary in the event we get anything unexpected:
let properties: [String: GeneratedContent] = {
if case let .structure(properties, _) = subComponentsContent.kind {
return properties
}
return [:]
}()
Finally, we build out our result struct's subComponents
field by mapping over those properties.
subComponents = try properties.map { (title, bodyContent) in
try SubComponentResult(title: title, body: bodyContent.value(String.self))
}
Two things are admittedly weird about that last bit:
- I got a little lazy here by using the each sub-components'
title
as the name of the corresponding generated property. Since the property name gets fed into the LLM, one can only imagine doing so can only improve the results. Based on my experience so far, the name of a field greatly influences what kind of data you get back from the on-device foundation models. - The
bodyContent
itself is aGeneratedContent
that we know to be a string (again, because that's what our dynamic schema specifies), so we can safely demand one back using its value(Type) method
5. Pull it all together
Okay, the moment of truth. This shit compiles, but will it work? At least as of OS 26 betas 5 & 6: yes!
My aforementioned Swift file ends with a #Playground
you can actually futz with interactively in Xcode 26 and navigate the results interactively. Just three more calls to get your cocktail:
import Playgrounds
#Playground {
let session = LanguageModelSession {
cocktailPromptSet.instructions
}
let response = try await session.respond(
to: "Shirley Temple",
schema: GenerationSchema(root: cocktailSchema, dependencies: [])
)
let cocktailResult = try EducationalResult(response.content)
}
The above yielded this response:
EducationalResult(
summary: "The Shirley Temple is a classic and refreshing cocktail that has been delighting children and adults alike for generations. It\'s known for its simplicity, sweet taste, and vibrant orange hue. Made primarily with ginger ale, it\'s a perfect example of a kid-friendly drink that doesn\'t compromise on flavor. The combination of ginger ale and grenadine creates a visually appealing and sweet-tart beverage, making it a staple at parties, brunches, and any occasion where a fun and easy drink is needed.",
confidence: 100,
subComponents: [
SubComponentResult(title: "Steps", body: "1. In a tall glass filled with ice, pour 2 oz of ginger ale. 2. Add 1 oz of grenadine carefully, swirling gently to combine. 3. Garnish with an orange slice and a cherry on top."),
SubComponentResult(title: "Prep", body: "Ensure you have fresh ginger ale and grenadine ready to go."),
SubComponentResult(title: "Ingredients", body: "2 oz ginger ale, 1 oz grenadine, Orange slice, Cherry")
])
The best part? I can only generate "Shirley Temple" drinks because whenever I ask for an alcoholic cocktail, it trips the on-device models' safety guardrails and refuses to generate anything.
Cool!
This was too hard
I've heard stories about Apple's documentation being bad, but never about it being straight-up wrong. Live by the beta, die by the beta, I guess.
In any case, between the documentation snafu and Claude Code repeatedly shitting the bed trying to guess its way through this API, I'm actually really grateful I was forced to buckle down and learn me some Swift.
Let me know if this guide helped you out! 💜