The Vercel AI SDK’s generateObject function turns fuzzy LLM text into typed, validated data without hand-rolled JSON parsing. If you need reliable vercel ai sdk generateobject structured output, the key is a strict schema and a provider configuration that surfaces failures instead of swallowing them. This guide walks through a production-ready setup from scratch, using Zod for contracts and an OpenAI-compatible endpoint for model access.
Step 1: Scaffold the project and install dependencies
Create a fresh Node project and add the required packages. The ai core package ships generateObject; @ai-sdk/openai adapts any OpenAI-compatible API; zod defines the schema. Avoid dragging in the entire Vercel framework—these packages are framework-agnostic and run in any Node 18+ environment.
mkdir structured-llm && cd structured-llm
npm init -y
npm install ai @ai-sdk/openai zod dotenv
npm install -D tsx typescript @types/node
Set up a minimal tsconfig.json so you can run TypeScript directly with tsx without a build step:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
Create a .env file for your API key. Never hard-code credentials in source; the SDK reads process.env via dotenv.
echo "OPENAI_API_KEY=sk-your-key" > .env
Step 2: Define the structured schema with Zod
The schema is the contract. generateObject will coerce the model output to match it and throw if the model drifts. Use Zod’s rich validators to reject bad data early. A well-designed schema is what makes vercel ai sdk generateobject structured output trustworthy—if you leave fields loosely typed, you move the validation burden back into your business logic.
import { z } from 'zod';
export const InvoiceSchema = z.object({
vendor: z.string().min(1),
invoiceNumber: z.string().regex(/^INV-\d+$/),
totalCents: z.number().int().nonnegative(),
currency: z.enum(['USD', 'EUR', 'GBP']),
lineItems: z.array(
z.object({
amountCents: z.number().int().nonnegative(),
})
),
});
export type Invoice = z.infer<typeof InvoiceSchema>;
Two practical notes from shipping this: first, prefer .coerce.number() when the model tends to emit strings like "1500"; second, keep regexes simple. Complex patterns increase validation failures and retry loops. If a field is optional in the source document but required downstream, use z.string().optional() and handle the undefined case in code rather than forcing the model to hallucinate.
Step 3: Configure the model provider
The SDK abstracts model access behind a LanguageModel interface. For OpenAI-compatible gateways, use createOpenAI and point baseURL at your endpoint. This works for OpenAI, a local LLM, or an aggregator.
import { createOpenAI } from '@ai-sdk/openai';
import { config } from 'dotenv';
config();
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY!,
baseURL: 'https://api.openai.com/v1',
});
If you want one endpoint that fronts 240+ models with automatic fallback when a provider is rate-limited or degraded, point baseURL at n4n.ai’s OpenAI-compatible gateway instead. The SDK calls are identical; you only change the URL and the model identifier string.
const gateway = createOpenAI({
apiKey: process.env.N4N_API_KEY!,
baseURL: 'https://api.n4n.ai/v1',
});
// gateway('anthropic/claude-3.5-sonnet') or gateway('openai/gpt-4o')
Model compatibility matters: some endpoints require mode: 'json' while others use tool-calling under the hood. The Vercel AI SDK picks the right mode automatically for known providers, but when using an unknown gateway, set mode: 'json' explicitly if you see empty responses.
Step 4: Call generateObject and extract typed data
Build a prompt that instructs the model to populate the schema. generateObject injects the JSON schema automatically, but a clear task description reduces retries. Set temperature: 0 for deterministic extraction—there is no reason to add randomness to a parsing task.
import { generateObject } from 'ai';
import { InvoiceSchema } from './schema';
import { openai } from './provider';
const prompt = `
Extract the invoice details from the following text:
"INV-10293 from Acme Corp for 3 widgets at 500 cents each, total 1500 cents USD."
`;
const { object, usage } = await generateObject({
model: openai('gpt-4o-mini'),
schema: InvoiceSchema,
prompt,
temperature: 0,
maxRetries: 2,
});
// object is typed as Invoice
console.log(object.vendor); // "Acme Corp"
console.log(usage); // { promptTokens, completionTokens, totalTokens }
The returned object is already parsed and validated. If the model returns malformed JSON, the SDK retries per maxRetries before throwing. This is the core of vercel ai sdk generateobject structured output: you get a typed object, not a string to JSON.parse and hope.
Step 5: Handle validation errors and partial failures
Models occasionally emit fields that violate the schema (e.g., a currency outside your enum). generateObject throws TypeValidationError in that case. Catch it and either repair or fall back to unstructured extraction.
import { generateObject, TypeValidationError } from 'ai';
try {
const { object } = await generateObject({
model: openai('gpt-4o-mini'),
schema: InvoiceSchema,
prompt,
temperature: 0,
});
return object;
} catch (err) {
if (err instanceof TypeValidationError) {
console.error('Schema mismatch:', err.cause);
// Send the error back to the model with generateText for a fix pass,
// or return a safe default. Do not silently swallow.
throw new Error('LLM produced invalid structure');
}
throw err;
}
For high-stakes pipelines, add a second pass: send the validation error back to the model with generateText and ask it to fix the specific field. Keep maxRetries low (1–2) to avoid latency spikes. Also log usage to track cost per extraction; per-token metering is only useful if you record it.
Step 6: Verify success end to end
Write a small runner script and execute it with tsx. Assert the shape and print the result. This is your smoke test before wiring into a service.
// run.ts
import { generateObject } from 'ai';
import { openai } from './provider';
import { InvoiceSchema } from './schema';
async function main() {
const { object } = await generateObject({
model: openai('gpt-4o-mini'),
schema: InvoiceSchema,
temperature: 0,
prompt: 'Invoice INV-999 from Globex: 2 licenses at 1000 cents EUR each.',
});
if (object.invoiceNumber !== 'INV-999') throw new Error('bad parse');
console.log(JSON.stringify(object, null, 2));
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
npx tsx run.ts
Expected output:
{
"vendor": "Globex",
"invoiceNumber": "INV-999",
"totalCents": 2000,
"currency": "EUR",
"lineItems": [
{ "description": "licenses", "amountCents": 1000 },
{ "description": "licenses", "amountCents": 1000 }
]
}
If you see a typed object printed without exceptions, your vercel ai sdk generateobject structured output pipeline works. Add a Vitest unit test that mocks the provider with mockLanguageModel from ai/test to lock this behavior in CI.
Step 7: Expose it as an API route
In a real system, the extraction lives behind an endpoint. Here is a minimal Express handler:
import express from 'express';
import { generateObject } from 'ai';
import { openai } from './provider';
import { InvoiceSchema } from './schema';
const app = express();
app.use(express.json());
app.post('/extract', async (req, res) => {
try {
const { object } = await generateObject({
model: openai('gpt-4o-mini'),
schema: InvoiceSchema,
temperature: 0,
prompt: req.body.text,
});
res.json(object);
} catch (err) {
res.status(422).json({ error: 'extraction_failed' });
}
});
app.listen(3000);
This keeps the schema at the boundary. Clients receive validated JSON or a clean 422—never a half-parsed string.
Production notes: streaming and cache control
For chat UIs, swap generateObject for streamObject to get incremental partial objects. The SDK emits partialObjectStream that you can pipe to the client over SSE.
import { streamObject } from 'ai';
const { partialObjectStream } = await streamObject({
model: openai('gpt-4o-mini'),
schema: InvoiceSchema,
prompt,
temperature: 0,
});
for await (const partial of partialObjectStream) {
process.stdout.write(JSON.stringify(partial) + '\n');
}
When routing through a gateway that honors client directives, pass cache hints via provider headers. The Vercel AI SDK forwards headers to the underlying fetch:
const { object } = await generateObject({
model: gateway('openai/gpt-4o'),
schema: InvoiceSchema,
prompt,
headers: { 'x-cache-ttl': '3600' }, // forwarded if gateway supports it
});
Stick to schema-first design, keep retries bounded, and treat validation errors as first-class. That discipline is what separates a demo from a system that survives real documents in production.