Skip to content
47% off: the complete course for $79 (reg. $149)Order now →
Agent Builder
Module 5Full course15 min read

Validating Tool Inputs and Outputs with Zod

Stop trusting model-generated arguments: validate every tool call with schemas, return errors the model can fix, and keep tool results tidy.

Your agent's tools now touch real files and real websites, and every argument they receive was written by a language model. Most of the time those arguments are perfect. Occasionally a field is missing, a number arrives as a string, or a path is blank. When that happens, the tools from the last lesson either crash with a confusing message or, worse, quietly do the wrong thing.

In this lesson you'll stop trusting model-generated arguments. You'll describe each tool's input once, with a Zod schema, and use that single description both to tell the model what to send and to check what it actually sent. You'll write validation errors the model can fix on its next try, validate the data that comes back from outside APIs, and make every tool result predictable.

The bug you already shipped

Look again at write_file from Adding Real Tools:

const content = typeof input.content === "string" ? input.content : "";

That line looks defensive. It isn't. If the model ever sends a write_file call without content (say, because a long response was cut off, or it got confused mid-task), this tool writes an empty file, overwrites whatever was there, and reports success. The model reads "Wrote 0 bytes to notes/plan.md" and carries on. Nobody finds out until later.

That's the general failure pattern with hand-written argument checks: each tool checks a little, differently, and the gaps are invisible. Model-generated input can go wrong in three ways, and each needs a different defense:

Kind of problemExampleWho can catch it
Wrong shapecontent missing, path sent as a numberA schema
Out of boundsan empty city name, a 50 MB file, a file:// URLA schema with limits
Wrong meaning../../.ssh/id_rsa, a URL pointing at your routerYour code, inside the tool

A schema handles the first two rows completely and consistently. The third row stays in your code, which is exactly where safePath and the local-address check already live.

✕ Before

A missing argument becomes a silent, destructive default:

const content = typeof input.content === "string" ? input.content : "";
await fs.writeFile(target, content, "utf8"); // empty file, "success"
✓ After

A missing argument becomes an error the model can fix:

Invalid input for write_file:
- content: Invalid input: expected string, received undefined
Fix these fields and call write_file again.