---
title: "Structured Output"
description: "Use the AI SDK's generateObject function to extract structured data from reviews. Define Zod schemas for type-safe structured output and display pros, cons, and key themes alongside summaries."
canonical_url: "https://vercel.com/academy/ai-summary-app-with-nextjs/structured-output"
md_url: "https://vercel.com/academy/ai-summary-app-with-nextjs/structured-output.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T14:25:23.954Z"
content_type: "lesson"
course: "ai-summary-app-with-nextjs"
course_title: "Creating an AI Summary App with Next.js"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Structured Output

# Structured output

Text summaries are great, but structured data opens new possibilities. Extract specific insights—pros, cons, key themes—in a format you can filter, sort, and display in creative ways. The AI SDK's `generateObject` with Zod schemas makes this type-safe and reliable.

## Outcome

Use `generateObject` to extract structured insights (pros, cons, themes) from reviews with full type safety using Zod schemas.

## Fast Track

1. Add `ReviewInsightsSchema` to `lib/types.ts` with `pros`, `cons`, `themes` arrays using `.describe()` hints
2. Create `getReviewInsights(product)` in `lib/ai-summary.ts` using `generateObject({ schema: ReviewInsightsSchema })`
3. Create `components/review-insights.tsx` with two-column pros/cons grid and theme tags, add to product page

## Hands-on Exercise 2.5

Extract structured insights from reviews:

**Requirements:**

1. Create a Zod schema for review insights (pros, cons, themes)
2. Add a `getReviewInsights` function using `generateObject`
3. Display pros and cons in a two-column layout
4. Show key themes as tags/badges
5. Keep the existing summary (don't replace it)

**Implementation hints:**

- `generateObject` requires a Zod schema as `schema` parameter
- The function returns typed data matching your schema
- Use arrays for pros/cons/themes (3-5 items each)
- Display insights in a Card below the AI summary
- Consider using a grid layout for pros/cons columns

## Understanding generateObject

The AI SDK provides `generateObject` for structured data extraction:

```typescript
import { generateObject } from "ai";
import { z } from "zod";

const schema = z.object({
  pros: z.array(z.string()),
  cons: z.array(z.string()),
});

const { object } = await generateObject({
  model: "anthropic/claude-sonnet-4.5",
  schema,
  prompt: "Extract pros and cons from these reviews...",
});

// object is fully typed: { pros: string[], cons: string[] }
```

**Benefits:**

- Type-safe output (TypeScript knows the structure)
- Automatic validation (Zod ensures correct format)
- Structured data (easy to filter, sort, display)

## Step 1: Define Insights Schema

Add to `lib/types.ts`:

```typescript title="lib/types.ts" {27-34}
import { z } from "zod";

// Review schema
export const ReviewSchema = z.object({
  reviewer: z.string(),
  stars: z.number().min(1).max(5),
  review: z.string(),
  date: z.string(),
});

// Product schema
export const ProductSchema = z.object({
  slug: z.string(),
  name: z.string(),
  description: z.string(),
  reviews: z.array(ReviewSchema),
});

// Infer TypeScript types
export type Review = z.infer<typeof ReviewSchema>;
export type Product = z.infer<typeof ProductSchema>;

// Review insights schema
export const ReviewInsightsSchema = z.object({
  pros: z.array(z.string()).describe("Positive aspects mentioned in reviews"),
  cons: z.array(z.string()).describe("Negative aspects or concerns"),
  themes: z.array(z.string()).describe("Key themes across all reviews"),
});

export type ReviewInsights = z.infer<typeof ReviewInsightsSchema>;
```

## Step 2: Create Insights Function

Add `generateObject` to your imports and the `getReviewInsights` function to `lib/ai-summary.ts`:

```typescript title="lib/ai-summary.ts" {1,2,17-50}
import { generateText, generateObject, streamText } from "ai";
import { Product, ReviewInsights, ReviewInsightsSchema } from "./types";

function buildSummaryPrompt(product: Product): string {
  // ... (existing prompt helper from 2.4)
}

export function streamReviewSummary(product: Product) {
  // ... (existing streaming function from 2.4)
}

export async function summarizeReviews(product: Product): Promise<string> {
  // ... (existing blocking function from 2.3)
}

export async function getReviewInsights(
  product: Product
): Promise<ReviewInsights> {
  const averageRating =
    product.reviews.reduce((acc, review) => acc + review.stars, 0) /
    product.reviews.length;

  const prompt = `Analyze the following customer reviews for the ${product.name} product (average rating: ${averageRating}/5).

Extract:
1. Pros: 3-5 positive aspects customers appreciate
2. Cons: 3-5 negative aspects or concerns mentioned
3. Themes: 3-5 key themes that emerge across reviews

Be specific and concise. Each item should be 3-7 words.

Reviews:
${product.reviews
    .map((review, i) => `Review ${i + 1} (${review.stars} stars):\n${review.review}`)
    .join("\n\n")}`;

  try {
    const { object } = await generateObject({
      model: "anthropic/claude-sonnet-4.5",
      schema: ReviewInsightsSchema,
      prompt,
    });

    return object;
  } catch (error) {
    console.error("Failed to extract insights:", error);
    throw new Error("Unable to extract review insights. Please try again.");
  }
}
```

**What changed:**

- Added `generateObject` to imports (line 1)
- Added `ReviewInsights` and `ReviewInsightsSchema` to type imports (line 2)
- Added new `getReviewInsights` function at the end of the file

## Step 3: Create Insights Component

Create `components/review-insights.tsx`:

```tsx title="components/review-insights.tsx"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Product } from "@/lib/types";
import { getReviewInsights } from "@/lib/ai-summary";

export async function ReviewInsights({ product }: { product: Product }) {
  const insights = await getReviewInsights(product);

  return (
    <Card className="w-full max-w-prose">
      <CardHeader>
        <CardTitle className="text-lg">Key Insights</CardTitle>
      </CardHeader>
      <CardContent className="space-y-6">
        {/* Pros and Cons Grid */}
        <div className="grid md:grid-cols-2 gap-6">
          {/* Pros */}
          <div>
            <h3 className="text-sm font-semibold mb-3 text-green-700 dark:text-green-400">
              Pros
            </h3>
            <ul className="space-y-2">
              {insights.pros.map((pro, i) => (
                <li key={i} className="text-sm flex items-start gap-2">
                  <span className="text-green-600 mt-0.5">✓</span>
                  <span className="text-muted-foreground">{pro}</span>
                </li>
              ))}
            </ul>
          </div>

          {/* Cons */}
          <div>
            <h3 className="text-sm font-semibold mb-3 text-red-700 dark:text-red-400">
              Cons
            </h3>
            <ul className="space-y-2">
              {insights.cons.map((con, i) => (
                <li key={i} className="text-sm flex items-start gap-2">
                  <span className="text-red-600 mt-0.5">✗</span>
                  <span className="text-muted-foreground">{con}</span>
                </li>
              ))}
            </ul>
          </div>
        </div>

        {/* Themes */}
        <div>
          <h3 className="text-sm font-semibold mb-3">Key Themes</h3>
          <div className="flex flex-wrap gap-2">
            {insights.themes.map((theme, i) => (
              <span
                key={i}
                className="px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full text-xs"
              >
                {theme}
              </span>
            ))}
          </div>
        </div>
      </CardContent>
    </Card>
  );
}
```

## Step 4: Add to Product Page

Update `app/[productId]/page.tsx`:

```tsx title="app/[productId]/page.tsx" {6,34}
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getProduct, getProducts } from "@/lib/sample-data";
import { Reviews } from "@/components/reviews";
import { StreamingSummary } from "@/components/streaming-summary";
import { ReviewInsights } from "@/components/review-insights";

export default async function ProductPage({
  params,
}: {
  params: Promise<{ productId: string }>;
}) {
  const { productId } = await params;

  let product;
  try {
    product = getProduct(productId);
  } catch {
    notFound();
  }

  return (
    <main className="min-h-screen p-8">
      <div className="max-w-4xl mx-auto space-y-8">
        <div>
          <h1 className="text-4xl font-bold">{product.name}</h1>
          <p className="text-lg text-muted-foreground mt-2">
            {product.description}
          </p>
        </div>

        <StreamingSummary product={product} />
        <ReviewInsights product={product} />

        <Reviews product={product} />
      </div>
    </main>
  );
}

// ... (generateStaticParams and generateMetadata remain the same)
```

## Try It

1. **Visit a product page:**
   ```
   http://localhost:3000/mower
   ```

2. **You should see:**
   - AI Summary card (existing)
   - **New: Key Insights card** with:
     - Pros column (green checkmarks)
     - Cons column (red X marks)
     - Theme tags at the bottom

3. **Example output for Mower3000:**

   **Pros:**

   - ✓ Quiet operation
   - ✓ Autonomous cutting
   - ✓ Good app integration
   - ✓ Quality mulching

   **Cons:**

   - ✗ Struggles on slopes
   - ✗ Boundary wire setup difficult
   - ✗ Gets stuck occasionally
   - ✗ Limited customer support

   **Themes:**

   - Autonomous Operation | Slope Challenges | Setup Complexity | Quiet Performance

4. **Check AI Gateway dashboard:**
   - Now making 2 API calls per product page
   - One for summary (`generateText`)
   - One for insights (`generateObject`)
   - Combined cost: \~$0.004 per page load

## How generateObject Works

**Request:**

```typescript
generateObject({
  schema: ReviewInsightsSchema,
  prompt: "Extract pros, cons, themes...",
})
```

**Behind the scenes:**

1. AI SDK sends your Zod schema to Claude
2. Claude generates structured JSON matching the schema
3. AI SDK validates the response against your schema
4. Returns typed object (TypeScript knows the structure)

**Response:**

```typescript
{
  pros: ["Quiet operation", "Autonomous cutting", ...],
  cons: ["Struggles on slopes", "Setup difficult", ...],
  themes: ["Autonomous Operation", "Slope Challenges", ...]
}
```

Fully typed. TypeScript autocomplete works. Runtime validation ensures correctness.

## Type Safety Benefits

**Without Zod:**

```typescript
const data: any = await callAI(); // Hope it has the right shape
const pros = data.pros; // Maybe? Could be undefined or wrong type
```

**With Zod and generateObject:**

```typescript
const { object } = await generateObject({
  schema: ReviewInsightsSchema,
  // ...
});

// TypeScript knows:
object.pros;    // string[]
object.cons;    // string[]
object.themes;  // string[]

// Runtime: Zod validates before returning
// If AI returns wrong shape, error is caught immediately
```

## Schema Descriptions

Notice the `.describe()` calls:

```typescript
pros: z.array(z.string()).describe("Positive aspects mentioned in reviews")
```

These descriptions are sent to the AI to guide extraction. More descriptive schemas = better results.

## Performance Note

**Current behavior:**

- 2 API calls per page load (summary + insights)
- \~4 seconds total generation time
- \~$0.004 per page load

**Coming in Section 3:**

- Smart caching reduces this to 1-time cost
- Subsequent loads: instant (cached)
- 97% cost reduction

## Commit

```bash
git add lib/types.ts lib/ai-summary.ts components/review-insights.tsx app/\[productId\]/page.tsx
git commit -m "feat(ai): add structured output with generateObject"
git push
```

## Done-When

- [ ] `ReviewInsightsSchema` defined in types
- [ ] `getReviewInsights` function using `generateObject`
- [ ] `ReviewInsights` component displays pros/cons/themes
- [ ] Insights appear on all product pages
- [ ] Data is fully type-safe
- [ ] Pros/cons displayed in two-column grid
- [ ] Themes shown as tags

## What's Next

You now have both text summaries and structured insights. But every page load costs tokens. In Section 3, you'll add Next.js 16 smart caching to generate once and reuse, reducing costs by 97% while maintaining great UX.

***

**Sources:**

- [AI SDK generateObject](https://sdk.vercel.ai/docs/reference/ai-sdk-core/generate-object)
- [Zod Schemas](https://zod.dev)
- [Structured Output Best Practices](https://docs.anthropic.com/claude/docs/tool-use)


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
