Structured Output
Get a typed object back instead of a string. Describe the shape with a JSON Schema and generateObject() handles the rest.
Overview
generateObject() turns free-form model output into a validated, typed value. You pass a JSON Schema describing the shape you want; the model is prompted to produce it, the JSON is extracted and validated against the schema, and on a mismatch the error is fed back and the model is re-prompted. It works on every backend — Apple Foundation Models, ML Kit, and downloadable Gemma / Qwen / Phi.
When to use it
Reach for generateObject() when the JSON is the answer — extraction, classification, or any “give me X as a struct” task. When you instead want the model to call a function and use the result, see Tool Calling.
How It Works
Structured output is orchestrated in JavaScript over sendMessage(), so it honors the same single-flight guard, systemPrompt, and AbortSignal semantics. Each call:
- Appends a strict JSON-Schema instruction to the system prompt
- Runs the model and extracts JSON from its output (tolerating prose and
```jsonfences) - Validates the value against a pragmatic subset of JSON Schema
- On a parse or schema mismatch, feeds the error back and re-prompts (up to
maxRepairAttempts)
Your First Object
Pass the conversation and a schema. The result is { object, text } — the validated value plus the raw output that produced it.
import { generateObject } from 'expo-ai-kit';
type Recipe = { title: string; minutes: number; ingredients: string[] };
const { object } = await generateObject<Recipe>(
[{ role: 'user', content: 'A quick weeknight pasta.' }],
{
type: 'object',
properties: {
title: { type: 'string' },
minutes: { type: 'integer' },
ingredients: { type: 'array', items: { type: 'string' } },
},
required: ['title', 'minutes', 'ingredients'],
},
);
object.title; // string
object.minutes; // number
object.ingredients; // string[]The Schema
A pragmatic subset is enforced locally: type, properties, required, items, enum, and type unions. Other keywords you include (like description or minLength) are still sent to the model to guide it, but are not validated on-device.
// enum + nested arrays + a passthrough description
const { object } = await generateObject(
[{ role: 'user', content: 'Classify: "the package never arrived"' }],
{
type: 'object',
properties: {
sentiment: { type: 'string', enum: ['positive', 'neutral', 'negative'] },
topics: { type: 'array', items: { type: 'string' } },
urgent: { type: 'boolean', description: 'true if it needs a fast reply' },
},
required: ['sentiment', 'urgent'],
},
);Keep schemas small and shallow
On-device models follow flat, shallow shapes far more reliably than deeply nested ones. Prefer a handful of top-level fields over deep object trees, and split complex extractions into multiple calls.
Repair & Retries
If the model returns invalid JSON or a value that violates the schema, the error is fed back and the model is asked to correct it — up to maxRepairAttempts times (default 2, i.e. up to 3 generations total). Lower it to fail fast, or raise it for stubborn schemas.
const { object, text } = await generateObject(messages, schema, {
maxRepairAttempts: 3,
systemPrompt: 'You extract structured data from support tickets.',
});Error Handling
If no schema-valid JSON is produced after the repair attempts, generateObject() throws a ModelError with code INFERENCE_FAILED. It also propagates INFERENCE_BUSY (a generation is already running) and INFERENCE_CANCELLED (the signal fired).
import { generateObject, ModelError } from 'expo-ai-kit';
try {
const { object } = await generateObject(messages, schema);
} catch (e) {
if (e instanceof ModelError && e.code === 'INFERENCE_FAILED') {
// Model couldn't produce valid JSON — fall back to plain text, or retry.
}
}Tips
- Mark every field you truly need in
required— optional fields are often omitted by small models. - Use
enumfor classification — it constrains the model far better than a free-form string. - Pass a focused
systemPromptdescribing the task; the schema instruction is appended to it automatically. - Read
text(the raw output) when debugging why a value didn't validate.
Stable by design
The call signature is intentionally stable, so native constrained decoding (Apple guided generation / LiteRT-LM) can slot in behind generateObject() later with no change to your code.