Generating Structured Objects (JSON) with Vercel AI SDK and Zod - Part 5 (Final)
In previous parts, we've seen how the Vercel AI SDK can generate and stream text. But what if you need the AI to output data in a specific, structured format, like JSON?

Technologies Used
see Part 4 here
This is crucial for tasks such as:
- Extracting information from text into a predefined schema.
- Generating configuration files.
- Populating databases with AI-generated content.
- Powering UIs that expect specific data structures.
Manually prompting an LLM to produce valid JSON and then parsing it can be error-prone. The Vercel AI SDK offers a powerful solution: generateObject
. This function, often used with zod
for schema definition and validation, ensures the LLM's output conforms to your desired structure.
Prerequisites
- A Next.js project setup.
- Vercel AI SDK and an LLM provider library installed.
- OpenAI API key (or equivalent for a model that supports structured output/function calling well).
zod
installed:npm install zod
orpnpm add zod
.
Core Concept: generateObject
The generateObject function works by leveraging the "tool/function calling" capabilities of advanced LLMs.
- You define a
zod
schema for the object you want. - You provide a prompt instructing the AI on what data to generate.
generateObject
essentially tells the LLM to "call a function" whose parameters match your Zod schema.- The LLM attempts to fill in these parameters based on your prompt.
- The Vercel AI SDK validates the output against the schema and returns the parsed, typed object.
Step 1: Defining a Zod Schema
Let's say we want to extract contact information from a piece of text. We can define a Zod schema for this:
// lib/schemas.ts (or directly in your API route)
import { z } from 'zod';
export const contactSchema = z.object({
name: z.string().optional().describe("The full name of the person."),
email: z.string().email().optional().describe("The email address."),
phone: z.string().optional().describe("The phone number."),
company: z.string().optional().describe("The company name."),
});
export type Contact = z.infer<typeof contactSchema>;
describe()
: Adding descriptions to your Zod schema fields can significantly help the LLM understand what to extract for each field.
Step 2: Creating the API Route for Object Generation
Create app/api/generate-object/route.ts
:
// app/api/generate-object/route.ts
import { openai } from '@ai-sdk/openai';
import { generateObject, GenerateObjectResult } from 'ai';
import { z } from 'zod';
export const runtime = 'edge'; // Can also be 'nodejs' if necessary for the model/provider
export const maxDuration = 30;
// Define the schema directly here or import it
const contactSchema = z.object({
name: z.string().optional().describe("The full name of the person."),
email: z.string().email().optional().describe("The email address."),
phone: z.string().optional().describe("The phone number."),
company: z.string().optional().describe("The company name, if mentioned."),
});
export async function POST(req: Request) {
const { textInput } = await req.json();
if (!textInput) {
return new Response('Text input is required', { status: 400 });
}
try {
const { object, usage, finishReason, ...rest } = await generateObject({
model: openai('gpt-4o'), // A model that supports tool/function calling is required
schema: contactSchema,
prompt: `Extract the contact information from the following text: "${textInput}"`,
// mode: 'tool', // Default is 'tool', can also be 'json' for models supporting JSON mode directly
});
// `object` is now a typed object matching your Zod schema
console.log('Generated Object:', object);
console.log('Usage:', usage);
console.log('Finish Reason:', finishReason);
// You can also access other parts of the result if needed, e.g., rawResponse
// console.log('Full result:', { object, usage, finishReason, ...rest });
return Response.json({ generatedContact: object, usage, finishReason });
} catch (error) {
console.error("Error generating object:", error);
let errorMessage = "Failed to generate object.";
if (error instanceof Error) {
errorMessage = error.message;
}
// Check if it's an APIError from the SDK for more details
// if (error instanceof APIError) { ... }
return new Response(JSON.stringify({ error: errorMessage }), { status: 500, headers: { 'Content-Type': 'application/json' } });
}
}
Explanation:
generateObject
: The core function.model
: An LLM instance that supports tool calling (e.g.,gpt-3.5-turbo
,gpt-4o
,Anthropic Claude models
).schema
: Your Zod schema.prompt
: The instruction to the LLM. It should guide the AI to extract or generate data fitting the schema.mode
: Can be 'tool
' (uses function calling) or 'json
' (uses the model's native JSON mode if available, like with newer OpenAI models). 'tool
' is generally more robust across models.
- The function returns an object containing the parsed
object
,usage
stats,finishReason
, and more. - Error Handling: It's important to wrap
generateObject
in a try-catch block as schema validation failures or LLM errors can occur.
Step 3: Building the UI for Object Generation (Client-Side)
Let's create a simple page app/object-gen/page.tsx
:
// app/object-gen/page.tsx
'use client';
import { FormEvent, useState } from 'react';
import { z } from 'zod'; // Import Zod for type inference if needed
// Re-define or import your schema for type safety on the client
const contactSchema = z.object({
name: z.string().optional(),
email: z.string().email().optional(),
phone: z.string().optional(),
company: z.string().optional(),
});
type Contact = z.infer<typeof contactSchema>;
interface GenerationResponse {
generatedContact?: Contact;
usage?: { promptTokens: number; completionTokens: number; totalTokens: number };
finishReason?: string;
error?: string;
}
export default function ObjectGenPage() {
const [textInput, setTextInput] = useState("John Doe works at Acme Corp. Reach him at john.doe@example.com or (555) 123-4567.");
const [generatedObject, setGeneratedObject] = useState<Contact | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [apiResponse, setApiResponse] = useState<GenerationResponse | null>(null);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError(null);
setGeneratedObject(null);
setApiResponse(null);
try {
const response = await fetch('/api/generate-object', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ textInput }),
});
const data: GenerationResponse = await response.json();
if (!response.ok) {
throw new Error(data.error || `API responded with ${response.status}`);
}
setGeneratedObject(data.generatedContact || null);
setApiResponse(data);
} catch (err) {
console.error(err);
setError(err instanceof Error ? err.message : 'An unknown error occurred.');
} finally {
setIsLoading(false);
}
};
return (
<div className="flex flex-col w-full max-w-xl py-12 mx-auto">
<h1 className="text-2xl font-bold mb-6">Structured Object Generation</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="text-input" className="block text-sm font-medium text-gray-700">
Text Input:
</label>
<textarea
id="text-input"
className="w-full p-2 border border-gray-300 rounded shadow-sm text-black"
rows={5}
value={textInput}
onChange={(e) => setTextInput(e.target.value)}
required
/>
</div>
<button
type="submit"
disabled={isLoading}
className="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 disabled:bg-gray-300"
>
{isLoading ? 'Generating...' : 'Generate Object'}
</button>
</form>
{error && (
<div className="mt-6 p-4 bg-red-100 text-red-700 border border-red-300 rounded">
<strong>Error:</strong> {error}
</div>
)}
{generatedObject && (
<div className="mt-6 p-4 border border-gray-200 rounded bg-gray-50">
<h3 className="text-lg font-semibold mb-2">Generated Object:</h3>
<pre className="whitespace-pre-wrap text-sm text-gray-800">
{JSON.stringify(generatedObject, null, 2)}
</pre>
</div>
)}
{apiResponse && apiResponse.usage && (
<div className="mt-2 p-2 border-gray-200 text-xs text-gray-600">
<p>Usage: {apiResponse.usage.totalTokens} tokens ({apiResponse.usage.promptTokens} prompt, {apiResponse.usage.completionTokens} completion). Finish: {apiResponse.finishReason}</p>
</div>
)}
</div>
);
}
Explanation:
- This client-side code makes a standard
fetch
request to our API endpoint. - It displays the generated object (if successful) or an error message.
- It's good practice to also show usage stats if relevant for your application.
Step 4: Testing Object Generation
Run pnpm dev
and navigate to your object generation page.
- Use the default text: "John Doe works at Acme Corp. Reach him at john.doe@example.com or (555) 123-4567."
- Click "Generate Object." You should see a JSON object with
name
,email
,phone
, andcompany
fields populated. - Try other inputs:
- "Meeting with Jane Smith (jane@coolstartup.io) next Tuesday." (Should extract name and email).
- "Generate a product spec for a 'smart water bottle' with features: temperature display, hydration reminders, and self-cleaning. Target price $50." (You'd need a different Zod schema for this, e.g.,
productSpecSchema
).
Key Takeaways
generateObject
combined with Zod schemas is a robust way to get structured JSON output from LLMs.- It significantly reduces the chances of malformed JSON or data that doesn't fit your expected structure.
- Clear Zod field descriptions (
.describe()
) help the LLM map information correctly. - Always handle potential errors, as LLM generation isn't always perfect, and schema validation might fail.
- This feature is incredibly powerful for integrating AI into data-driven workflows and UIs.
Series Conclusion
And that's a wrap on our introductory series to the Vercel AI SDK! We've covered a lot of ground:
- Building a basic chatbot with
useChat
andstreamText
. - Enhancing it with tool usage (function calling).
- Generating text completions with
useCompletion
. - Diving into advanced text generation and prompt engineering.
- Producing structured objects (JSON) with
generateObject
and Zod.
The Vercel AI SDK provides a comprehensive, developer-friendly toolkit for integrating a wide array of AI capabilities into your JavaScript applications. Its focus on streaming, first-class support for modern frameworks like Next.js, and thoughtful abstractions make it an excellent choice for building the next generation of AI-powered experiences.
The world of AI is rapidly evolving, and so is the Vercel AI SDK. We encourage you to explore the official documentation, experiment with different LLM providers, and continue learning. Happy building!