
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.
Here's what most AI agent code looks like:
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.
Define your schema first, then validate:
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:
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 validatedsafeParse returns a discriminated union — either { success: true, data: T } or { success: false, error: ZodError }. No exceptions, no ambiguity, no silent fallback to defaults.
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:
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,
});When the model returns a list of items, validate each one:
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.
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.
// 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
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:
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.
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