By Sagar Shankaran, Founder of CallSphere
Master Zod for building type-safe AI agent tools. Learn how to define schemas for tool inputs, validate LLM-generated arguments, parse structured outputs, and handle validation errors gracefully in TypeScript agent applications.
Key takeaways
LLMs generate structured output that your code must parse and execute. The model might return a function call with arguments like {"city": "San Francisco", "units": "celsius"} — or it might hallucinate malformed JSON, wrong field names, or invalid types. Without validation, these errors propagate silently into your tool execution layer.
Zod solves this by providing a single schema definition that serves as both runtime validator and TypeScript type generator. Define a schema once, and you get compile-time type checking, runtime validation, and JSON Schema generation for the LLM — all from the same source of truth.
Install Zod:
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
npm install zod
Define a schema and extract its TypeScript type:
import { z } from "zod";
const WeatherInputSchema = z.object({
city: z.string().min(1).describe("City name for weather lookup"),
units: z
.enum(["celsius", "fahrenheit"])
.default("celsius")
.describe("Temperature unit"),
includeForcast: z
.boolean()
.optional()
.describe("Whether to include a 5-day forecast"),
});
// Extract the TypeScript type automatically
type WeatherInput = z.infer<typeof WeatherInputSchema>;
// Result: { city: string; units: "celsius" | "fahrenheit"; includeForcast?: boolean }
The .describe() calls are critical for AI agents. These descriptions are included in the JSON Schema sent to the LLM, helping the model understand what each parameter expects.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
When the LLM returns tool call arguments, validate them before execution:
function executeToolCall(name: string, rawArgs: string) {
const schemas: Record<string, z.ZodSchema> = {
get_weather: WeatherInputSchema,
search_docs: SearchInputSchema,
create_ticket: TicketInputSchema,
};
const schema = schemas[name];
if (!schema) {
return { error: `Unknown tool: ${name}` };
}
const parsed = schema.safeParse(JSON.parse(rawArgs));
if (!parsed.success) {
// Return structured error to the LLM so it can retry
return {
error: "Invalid arguments",
details: parsed.error.issues.map((issue) => ({
path: issue.path.join("."),
message: issue.message,
})),
};
}
// parsed.data is fully typed here
return toolHandlers[name](parsed.data);
}
Using safeParse instead of parse prevents exceptions from crashing your agent loop. The structured error message can be sent back to the model so it can correct its arguments.
AI providers expect tool parameters as JSON Schema. Zod can generate this automatically using the zod-to-json-schema package:
import { zodToJsonSchema } from "zod-to-json-schema";
const jsonSchema = zodToJsonSchema(WeatherInputSchema, {
target: "openAi",
});
// Use in OpenAI tool definition
const tool = {
type: "function" as const,
function: {
name: "get_weather",
description: "Get current weather for a city",
parameters: jsonSchema,
},
};
This eliminates the need to manually write and maintain JSON Schema objects. When you update the Zod schema, the tool definition updates automatically.
Beyond tool inputs, Zod validates structured outputs from the LLM. When you ask the model to return JSON, validate that the response matches your expected format:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
const AnalysisOutputSchema = z.object({
sentiment: z.enum(["positive", "negative", "neutral"]),
confidence: z.number().min(0).max(1),
topics: z.array(z.string()).min(1),
summary: z.string().max(500),
});
async function analyzeText(text: string) {
const completion = await client.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "system",
content: "Analyze the following text and return JSON with sentiment, confidence, topics, and summary.",
},
{ role: "user", content: text },
],
response_format: { type: "json_object" },
});
const raw = JSON.parse(completion.choices[0].message.content ?? "{}");
const result = AnalysisOutputSchema.parse(raw);
return result; // Fully typed: { sentiment, confidence, topics, summary }
}
Real agent tools often need sophisticated schemas. Zod handles unions, recursive types, and transformations:
// Union types for different action kinds
const AgentActionSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("search"),
query: z.string(),
filters: z.record(z.string()).optional(),
}),
z.object({
type: z.literal("email"),
to: z.string().email(),
subject: z.string(),
body: z.string(),
}),
z.object({
type: z.literal("schedule"),
title: z.string(),
dateTime: z.string().datetime(),
attendees: z.array(z.string().email()),
}),
]);
// Transforms to coerce LLM output
const DateRangeSchema = z.object({
start: z.string().transform((s) => new Date(s)),
end: z.string().transform((s) => new Date(s)),
}).refine(
(data) => data.end > data.start,
{ message: "End date must be after start date" }
);
When validation fails, feed the error back to the LLM for self-correction:
async function executeWithRetry(
client: OpenAI,
messages: ChatCompletionMessageParam[],
schema: z.ZodSchema,
maxRetries = 2
): Promise<z.infer<typeof schema>> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const completion = await client.chat.completions.create({
model: "gpt-4o",
messages,
response_format: { type: "json_object" },
});
const raw = JSON.parse(completion.choices[0].message.content ?? "{}");
const result = schema.safeParse(raw);
if (result.success) return result.data;
// Append error as context for retry
messages.push(
{ role: "assistant", content: completion.choices[0].message.content ?? "" },
{
role: "user",
content: `Your response did not match the expected format. Errors: ${JSON.stringify(result.error.issues)}. Please try again.`,
}
);
}
throw new Error("Failed to get valid structured output after retries");
}
No. Zod validation is extremely fast for the small payloads typical of tool call arguments (microseconds). The overhead is negligible compared to the LLM API latency, which is measured in seconds.
Use Zod as your single source of truth and generate JSON Schema from it. This eliminates the risk of your TypeScript types drifting out of sync with the schema sent to the LLM. The zod-to-json-schema package handles the conversion reliably.
Use .optional() or .default() in your Zod schema. The .default() approach is usually better for agent tools because it ensures your execute function always receives a complete object without needing null checks.
#Zod #TypeScript #Validation #Schema #AIAgents #TypeSafety #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
A founder's guide to the personal AI assistant market: best AI assistant apps, business-grade options, and how CallSphere's voice agent fits in.
A founder's guide to free AI agents, low-code AI agent builders, and how to know when you should pay for a real platform like CallSphere.
Graphiti is the open-source temporal knowledge graph for AI agents in 2026. Learn how bi-temporal memory beats vector RAG for voice agents and long-running LLMs.
Chatbot app vs ChatGPT in 2026: a founder's clear take on the difference, when to use which, and how a real AI chatbot app development works.
How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.
Head-to-head: OpenAI Frontier and Anthropic's managed agent stack — strengths, fit, and what each means for enterprise AI voice and chat deployment.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI