---
title: "Structured Output"
description: "Extract structured, type-safe data from AI responses using Valibot schemas for reliable data extraction."
canonical_url: "https://vercel.com/academy/svelte-on-vercel/svelte-structured-output"
md_url: "https://vercel.com/academy/svelte-on-vercel/svelte-structured-output.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-03-14T05:35:04.057Z"
content_type: "lesson"
course: "svelte-on-vercel"
course_title: "Svelte on Vercel"
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 with Valibot Schemas

Tools let the AI *do* things. But sometimes you don't need the AI to do anything. You need it to *understand* something. Take a messy human sentence like "fresh pow at Palisades" and turn it into clean, typed JSON your app can trust. The AI SDK's `generateText()` with `Output.object()` does exactly this, reusing the same Valibot schemas you already have.

## Outcome

Build a `/api/parse-alert` endpoint that uses `generateText()` with `Output.object()` to extract structured alert data from natural language.

## Fast Track

1. Wrap a Valibot schema with `valibotSchema()` and pass it to `Output.object()`
2. Use `generateText()` with the `output` option to get typed, validated responses
3. Validate the result with Valibot's `parse()` for runtime safety

## Tools vs Structured Output

Both give you structured data from the AI. The difference:

|                    | Tools                             | Structured Output              |
| ------------------ | --------------------------------- | ------------------------------ |
| **Mechanism**      | Model calls a function            | Model returns a JSON object    |
| **Use when**       | You need to execute side effects  | You need pure data extraction  |
| **Streaming**      | Text streams alongside tool calls | Object streams as partial JSON |
| **Schema library** | Valibot with `valibotSchema()`    | Valibot with `valibotSchema()` |

The chat endpoint uses tools because creating an alert is a side effect. This new endpoint uses structured output because parsing text into data is pure extraction.

## The Valibot Schema

The ski-alerts app already defines alert schemas in `src/lib/schemas/alert.ts`:

```typescript title="src/lib/schemas/alert.ts" {1-18}
import * as v from 'valibot';

export const AlertConditionSchema = v.variant('type', [
  v.object({
    type: v.literal('snowfall'),
    operator: v.picklist(['gt', 'gte', 'lt', 'lte']),
    value: v.number(),
    unit: v.literal('inches')
  }),
  v.object({
    type: v.literal('temperature'),
    operator: v.picklist(['gt', 'gte', 'lt', 'lte']),
    value: v.number(),
    unit: v.picklist(['fahrenheit', 'celsius'])
  }),
  v.object({
    type: v.literal('conditions'),
    match: v.picklist(['powder', 'clear', 'snowing', 'windy'])
  })
]);
```

You'll use this same schema to constrain the AI's output.

## Hands-on exercise 2.3

Let's create a new endpoint that parses natural language alert descriptions into structured data:

**Requirements:**

1. Complete the endpoint at `src/routes/api/parse-alert/+server.ts`
2. Use `generateText()` with `Output.object()` and `valibotSchema(CreateAlertToolInputSchema)` for structured output
3. Accept a `query` string in the POST body (e.g., "powder at Mammoth")
4. Return the parsed alert condition as validated JSON
5. Validate the AI's output with Valibot's `parse()` before returning

**Implementation hints:**

- Import `Output` from `ai` and `valibotSchema` from `@ai-sdk/valibot`
- Reuse `CreateAlertToolInputSchema` from `$lib/schemas/alert`, the same schema you used for the tool
- After getting the AI result, validate it with `v.parse(AlertConditionSchema, output.condition)` for double safety
- Include the resort list in the prompt so the AI can resolve resort names to IDs

## Try It

1. **Test with curl:**

   ```bash
   $ curl -X POST http://localhost:5173/api/parse-alert \
     -H "Content-Type: application/json" \
     -d '{"query": "more than 6 inches of snow at Grand Targhee"}'
   ```

   Expected response:

   ```json
   {
     "resortId": "grand-targhee",
     "resortName": "Grand Targhee",
     "condition": {
       "type": "snowfall",
       "operator": "gt",
       "value": 6,
       "unit": "inches"
     },
     "originalQuery": "more than 6 inches of snow at Grand Targhee"
   }
   ```

2. **Test ambiguous input:**

   ```bash
   $ curl -X POST http://localhost:5173/api/parse-alert \
     -H "Content-Type: application/json" \
     -d '{"query": "fresh pow at Palisades"}'
   ```

   Expected:

   ```json
   {
     "resortId": "palisades",
     "resortName": "Palisades Tahoe",
     "condition": {
       "type": "conditions",
       "match": "powder"
     },
     "originalQuery": "fresh pow at Palisades"
   }
   ```

3. **Test invalid input:**

   ```bash
   $ curl -X POST http://localhost:5173/api/parse-alert \
     -H "Content-Type: application/json" \
     -d '{"query": "hello world"}'
   ```

   The AI should still attempt to parse it. If it can't extract a meaningful alert, the validation step catches it.

## Commit

```bash
git add -A
git commit -m "feat(parse): add structured output endpoint with Valibot validation"
git push
```

## Done-When

- [ ] `/api/parse-alert` accepts a natural language query and returns structured JSON
- [ ] Output matches the `AlertCondition` schema shape
- [ ] Resort names are resolved to IDs correctly
- [ ] Invalid inputs return a clear error response

## Solution

```typescript title="src/routes/api/parse-alert/+server.ts"
import { json } from '@sveltejs/kit';
import { createGateway, generateText, Output } from 'ai';
import { valibotSchema } from '@ai-sdk/valibot';
import * as v from 'valibot';
import { resorts } from '$lib/data/resorts';
import { CreateAlertToolInputSchema, AlertConditionSchema } from '$lib/schemas/alert';
import { AI_GATEWAY_API_KEY } from '$env/static/private';
import type { RequestHandler } from './$types';

const gateway = createGateway({
  apiKey: AI_GATEWAY_API_KEY
});

export const POST: RequestHandler = async ({ request }) => {
  const { query } = await request.json();

  if (!query || typeof query !== 'string') {
    return json({ error: 'query string required' }, { status: 400 });
  }

  const resortList = resorts
    .map((r) => `- ${r.name} (id: ${r.id})`)
    .join('\n');

  const { output } = await generateText({
    model: gateway('anthropic/claude-sonnet-4'),
    output: Output.object({
      schema: valibotSchema(CreateAlertToolInputSchema)
    }),
    prompt: `Parse this natural language alert request into structured data.

Available resorts:
${resortList}

Map common phrases:
- "fresh powder", "pow", "new snow" → conditions type with match: "powder"
- "snowing", "snowfall" with no amount → conditions type with match: "snowing"
- Specific amounts like "6 inches" → snowfall type with operator
- Temperature references → temperature type with operator

User request: "${query}"`
  });

  if (!output) {
    return json(
      { error: 'AI returned no structured output' },
      { status: 422 }
    );
  }

  // Validate the condition with Valibot for runtime type safety
  try {
    v.parse(AlertConditionSchema, output.condition);
  } catch {
    return json(
      { error: 'AI returned invalid condition structure' },
      { status: 422 }
    );
  }

  const resort = resorts.find((r) => r.id === output.resortId);

  return json({
    resortId: output.resortId,
    resortName: resort?.name ?? output.resortId,
    condition: output.condition,
    originalQuery: query
  });
};
```

`generateText()` with `Output.object()` constrains the model to output JSON matching the schema: no free-text, just data. We reuse the same `CreateAlertToolInputSchema` from the tool definition, so there's no schema duplication. The `v.parse()` call after the AI response adds a second validation layer for runtime safety. No streaming needed here. Structured output is a single request/response.

## Troubleshooting

\*\*Warning: AI returns null for the output\*\*

Your prompt may not be specific enough. Make sure you include the resort list and phrase mappings so the model has enough context to generate valid JSON. If the model can't figure out what the user wants, `Output.object()` returns `null`.

\*\*Warning: Valibot parse() throws after the AI response\*\*

The AI generated a condition that doesn't match the schema. Log the raw output with `console.log(output)` before the validation step to see what the model actually returned. Common issue: the model picks an operator or match value that isn't in the picklist.

## Advanced: Streaming Structured Output

For large objects, you can stream partial results with `streamText()` and `Output.object()`. The `partialOutputStream` async iterable emits progressively complete objects as the AI generates:

```typescript
import { streamText, Output } from 'ai';

const { partialOutputStream } = streamText({
  model: gateway('anthropic/claude-sonnet-4'),
  output: Output.object({
    schema: valibotSchema(CreateAlertToolInputSchema)
  }),
  prompt: `Parse: "${query}"`
});

for await (const partialObject of partialOutputStream) {
  console.log(partialObject); // Progressively complete object
}
```

Each iteration yields a more complete version of the final object. This is useful when the schema is large and you want to show progressive results in the UI.


---

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