← Back to Payloads
2026-05-20

Stop Parsing LLM Outputs Manually — Use Zod to Validate AI Responses

Your AI agent outputs garbage JSON half the time. Here's how to define exactly what structure you want and get compile-time guarantees that the response actually matches.
Quick Access
Install command
$ mrt install tutorial
Browse related skills
Stop Parsing LLM Outputs Manually — Use Zod to Validate AI Responses

Stop Parsing LLM Outputs Manually — Use Zod to Validate AI Responses

Every developer who has shipped an AI agent has been there: the model returns something slightly wrong. A field is missing. The number is a string. The enum has an value you didn't account for. You add another regex patch or try/catch, and six months later your code is held together by string manipulation and prayer.

There's a better way. Zod — the TypeScript-first schema validation library — turns this problem around. Instead of wrangling the output after the fact, you define the exact shape you want upfront, and Zod validates and type-guards the response for you. If the model can't produce valid output after a few retries, it fails loudly instead of silently corrupting your data pipeline.

This is not a new idea. But most AI tutorials skip it entirely, which means developers ship fragile parsing code that breaks in production on unexpected model outputs.

The Problem with Manual Parsing

Here's what most AI agent code looks like:

typescript
const response = await callModel(prompt);
const data = JSON.parse(response);
// Hope the model returned what we expected
const userId = data.user_id ?? data.userId ?? data['user-id'];
const score = parseInt(data.score, 10) || 0;

This breaks in subtle ways. The model might return user_id on success and userId on error. The score might be a float instead of an int. The status field might be "active" instead of "ACTIVE". You won't know until runtime, and by then your downstream code is already processing garbage.

The Zod Approach

Define your schema first, then validate:

typescript
import { z } from 'zod';
const UserProfile = z.object({
  userId: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(['admin', 'editor', 'viewer']),
  createdAt: z.string().datetime(),
  metadata: z.record(z.string(), z.unknown()).optional(),
});
type UserProfile = z.infer<typeof UserProfile>;

Now validation is explicit:

typescript
const rawResponse = await callModel(prompt);
const result = UserProfile.safeParse(rawResponse);
if (!result.success) {
  console.error('Model output invalid:', result.error.flatten());
  throw new Error(`Invalid response: ${result.error.message}`);
}
const profile: UserProfile = result.data; // fully typed, fully validated

safeParse returns a discriminated union — either { success: true, data: T } or { success: false, error: ZodError }. No exceptions, no ambiguity, no silent fallback to defaults.

Why This Matters for AI Agents Specifically

LLMs are probabilistic. They will occasionally return a valid JSON structure that is subtly wrong — a missing required field, an enum value from a previous version, a number formatted as a string. Your code should handle this gracefully, and Zod makes it the default behavior.

The other win is retry logic. If safeParse fails, you can retry the model call with a stricter prompt. If it fails twice, you have a proper error instead of corrupted state:

typescript
const profile = await withRetry(() => callModel(prompt), {
  maxAttempts: 3,
  onRetry: (attempt, error) => {
    console.warn(`Attempt ${attempt} failed: ${error.message}`);
    return extendPrompt(prompt, `Previous response was invalid. Error: ${error.message}`);
  },
  schema: UserProfile,
});

Validating Array Outputs

When the model returns a list of items, validate each one:

typescript
const Article = z.object({
  title: z.string().min(1).max(200),
  url: z.string().url(),
  publishedAt: z.string().datetime().optional(),
  tags: z.array(z.string()).min(1).max(10),
});
const ArticleList = z.object({
  articles: z.array(Article),
  total: z.number().int().nonnegative(),
  page: z.number().int().min(1).default(1),
});
const result = ArticleList.safeParse(rawResponse);

If the model invents a field or misformats a date, Zod catches it. If the array is empty when it shouldn't be, Zod catches it.

The Schema Is Documentation

One underrated benefit: your Zod schema is also your documentation. Anyone reading the code immediately understands what shape the AI is expected to produce, with exact constraints. No need to cross-reference the prompt and hope it's up to date.

typescript
// This schema tells you everything about the expected AI output:
// - userId must be a UUID string
// - role must be one of three values
// - createdAt must be ISO 8601 datetime
// - metadata is optional and can contain any values

Combining with System Prompts

The practical workflow: 1. Define your Zod schema as the source of truth 2. Include the schema in your system prompt using JSON Schema format 3. Validate the raw response with safeParse 4. Retry with an extended prompt on failure

For the prompt, generate a JSON Schema representation:

typescript
import { z } from 'zod';
const systemPrompt = `Return a JSON object matching this schema:
${JSON.stringify(UserProfile.json_schema, null, 2)}
Respond ONLY with valid JSON. Do not include markdown fences or explanation.`;

Zod's .json_schema() method generates a standard JSON Schema you can paste directly into prompts. This gives the model a precise target and dramatically reduces malformed output.

The Bottom Line

Stop patching LLM outputs with fallback logic and manual parsing. Define what you want, validate against it, and fail fast when the model can't deliver. Zod makes this the path of least resistance — which means your AI agents become significantly more reliable in production.

The first time you ship a new field and the model still returns the old shape, Zod will tell you exactly what's wrong. That's worth the 15 minutes of schema setup.

Mr. TECHNOLOGY

Related Dispatches