Structured Output
Generate typed, validated JSON objects from language models.
Generate typed, validated JSON objects from local language models using Zod schemas.
See it in action
Try Data Extractor and Research Agent for working demos of these APIs.
generateObject()
Generate a complete JSON object matching a schema:
import { generateObject, jsonSchema } from '@localmode/core';
import { webllm } from '@localmode/webllm';
import { z } from 'zod';
const { object } = await generateObject({
model: webllm.languageModel('Qwen3-1.7B-q4f16_1-MLC'),
schema: jsonSchema(z.object({
name: z.string(),
email: z.string(),
company: z.string().optional(),
})),
prompt: 'Extract contact info: "Hi, I\'m Sarah at sarah@acme.co, Acme Corp"',
});
console.log(object);
// { name: "Sarah", email: "sarah@acme.co", company: "Acme Corp" }Options
interface GenerateObjectOptions<T> {
model: LanguageModel; // The language model to use
schema: ObjectSchema<T>; // Schema defining expected output
prompt: string; // What to extract/generate
systemPrompt?: string; // Additional system instructions
mode?: 'json' | 'array' | 'enum'; // Output mode (default: 'json')
maxTokens?: number; // Max tokens (default: 1024)
temperature?: number; // Sampling temp (default: 0)
maxRetries?: number; // Validation retries (default: 3)
abortSignal?: AbortSignal; // Cancellation support
}Result
interface GenerateObjectResult<T> {
object: T; // Parsed, validated object
rawText: string; // Raw model output
finishReason: FinishReason; // stop, length, etc.
usage: GenerationUsage; // Token counts and duration
response: GenerationResponse; // Model ID and timestamp
attempts: number; // Retry attempts (1 = first try)
}streamObject()
Stream partial objects as tokens arrive, with final validation:
import { streamObject, jsonSchema } from '@localmode/core';
import { z } from 'zod';
const result = await streamObject({
model,
schema: jsonSchema(z.object({
name: z.string(),
items: z.array(z.string()),
})),
prompt: 'Generate a shopping list for a BBQ',
});
for await (const partial of result.partialObjectStream) {
console.log(partial);
// { name: "BBQ Shopping" }
// { name: "BBQ Shopping", items: ["burgers"] }
// { name: "BBQ Shopping", items: ["burgers", "buns", "ketchup"] }
}
const final = await result.object; // fully validatedWith Callback
const result = await streamObject({
model,
schema,
prompt: 'Extract data...',
onPartialObject: (partial) => {
// Update UI in real-time
updateDisplay(partial);
},
});jsonSchema()
Convert Zod schemas to ObjectSchema for use with generateObject and streamObject:
import { jsonSchema } from '@localmode/core';
import { z } from 'zod';
const schema = jsonSchema(z.object({
name: z.string().describe('Full name'),
age: z.number(),
tags: z.array(z.string()),
role: z.enum(['admin', 'user']),
bio: z.string().optional(),
}));Supported Zod Types
| Zod Type | JSON Schema Output |
|---|---|
z.string() | { type: "string" } |
z.number() | { type: "number" } |
z.boolean() | { type: "boolean" } |
z.array(z.string()) | { type: "array", items: { type: "string" } } |
z.object({...}) | { type: "object", properties: {...} } |
z.enum([...]) | { type: "string", enum: [...] } |
z.optional(...) | Omitted from required |
z.nullable(...) | { anyOf: [..., { type: "null" }] } |
z.literal(...) | { const: ... } |
z.union([...]) | { anyOf: [...] } |
jsonSchema() uses duck-typing to read Zod's internal structure. No Zod import is needed in @localmode/core — bring your own Zod.
Output Modes
JSON Mode (default)
Generate a single JSON object:
const { object } = await generateObject({
model,
schema: jsonSchema(z.object({ name: z.string() })),
prompt: 'Extract name from: "Hi, I\'m John"',
mode: 'json', // default
});Array Mode
Generate an array of objects:
const { object } = await generateObject({
model,
schema: jsonSchema(z.object({ item: z.string(), qty: z.number() })),
prompt: 'List 5 grocery items for a BBQ',
mode: 'array',
});
// [{ item: "burgers", qty: 8 }, { item: "buns", qty: 8 }, ...]Enum Mode
Generate one value from a set:
const { object } = await generateObject({
model,
schema: jsonSchema(z.enum(['positive', 'negative', 'neutral'])),
prompt: 'Classify sentiment: "I love this product!"',
mode: 'enum',
});
// "positive"Validation and Retry
generateObject() automatically retries when the model produces invalid output:
- Model generates text
- JSON is extracted (handles code fences, surrounding text)
- Parsed JSON is validated against the schema via
.parse() - On failure: the validation error is appended to the prompt and the model retries
- Up to
maxRetriesattempts (default: 3)
const { object, attempts } = await generateObject({
model,
schema,
prompt: 'Extract data...',
maxRetries: 5, // up to 5 attempts
});
console.log(`Succeeded on attempt ${attempts}`);If all attempts fail, a StructuredOutputError is thrown with the last validation error.
React Hook
Use useGenerateObject from @localmode/react:
import { useGenerateObject, jsonSchema } from '@localmode/react';
import { z } from 'zod';
const schema = jsonSchema(z.object({ name: z.string(), age: z.number() }));
function MyComponent() {
const { data, isLoading, error, execute } = useGenerateObject({
model,
schema,
});
return (
<div>
<button onClick={() => execute('Extract: John is 30')}>
Extract
</button>
{data && <pre>{JSON.stringify(data.object, null, 2)}</pre>}
</div>
);
}For full React hook documentation, see the React Hooks guide.
Use Cases
- Data extraction — Pull structured fields from emails, documents, web pages
- Form autofill — Extract form data from natural language descriptions
- Entity parsing — Convert unstructured text to typed entities
- Classification — Use enum mode as an LLM-powered classifier
- Config generation — Generate configuration objects from requirements
- Content structuring — Convert articles into structured metadata
Model Recommendations
| Model | Size | Structured Output Quality |
|---|---|---|
| Qwen 3 1.7B | 1.1GB | Good — reliable for simple schemas |
| Qwen 2.5 3B | 1.7GB | Better — handles nested objects well |
| Qwen 3 4B | 2.2GB | Best — complex schemas and arrays |
| Phi 3.5 Mini | 2.1GB | Excellent — strong instruction following |
Temperature defaults to 0 for structured output (not 0.7 like generateText). This ensures deterministic, valid JSON. Override with temperature option if needed.