Structured Output

Get a typed object back instead of a string. Describe the shape with a JSON Schema and generateObject() handles the rest.

iOSAndroid

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 ```json fences)
  • 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.

recipe.tstypescript
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 enum for classification — it constrains the model far better than a free-form string.
  • Pass a focused systemPrompt describing 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.