# Zod: stop bad data from a customer's API before your agent reads it

> On a customer site, one wrong field in an API response is enough to push an agent into a bad decision. A Zod schema at the entry point catches that error before the agent ever reads the data.

Original: https://fdetimes.net/en/tools/zod-validate-customer-api-data/

Writing an MCP server in TypeScript for a customer? Then Zod is in your project before you have had a chance to pick a library. The official documentation for the MCP TypeScript SDK states that the SDK requires `zod` as a mandatory peer dependency for schema validation.

That detail says a lot about how agent tools are built today: before data reaches the agent, it has to pass through a schema. For an FDE, the entry point that matters most is the customer's API. It is often a legacy system, thinly documented, and it sometimes returns things nobody expected.

Errors of this kind rarely crash the program straight away. They travel quietly into the prompt, into a calculation, into the agent's decision, and surface three days later as a wrong answer nobody can trace. Zod lets you stop the error at the door instead of hunting for it at the end of the pipeline.

## Zod does one job, but from both sides

The official site, zod.dev, describes Zod as a TypeScript-first schema validation library with static type inference. It was created by Colin McDonnell, is MIT-licensed and has zero external dependencies. Its API is immutable, and its core bundle is very small.

"From both sides" means this. At runtime, `.parse` checks the input and returns a typed deep clone, so unchecked data only becomes typed data once it has passed the schema.

At compile time, `z.infer` derives the TypeScript type from that same schema. You no longer have to maintain a hand-written `interface` alongside a separate validation function and worry about the two drifting apart.

## An order payload through a schema

Picture a retail chain as the customer, with your agent reading orders to answer questions from customer-service staff. The API documentation says `amount` is a number. Then one day the response comes back as `{ "id": "A-1029", "amount": "12.5", "status": "paid" }`: `amount` is a string.

```ts
import { z } from "zod";

const Order = z.object({
  id: z.string(),
  amount: z.number(),
  status: z.string(),
});

type Order = z.infer<typeof Order>;

const result = Order.safeParse(rawFromCustomerApi);

if (!result.success) {
  for (const issue of result.error.issues) {
    console.warn(issue.path, issue.code, issue.message);
  }
} else {
  runAgent(result.data); // result.data is typed as Order
}
```

There are two options here. `.parse` throws a `ZodError` when the input is wrong. `safeParse` does not throw; it returns a result object holding either the parsed data or a `ZodError`. With customer data, where errors are routine rather than exceptional, `safeParse` is usually the more sensible default.

The information in a `ZodError` is fairly detailed: each issue has a `code`, a `path` and a `message`. In the example above, `path` points straight at `amount`. Instead of telling the customer "the agent gave a wrong answer", you can say "the orders endpoint returns `amount` as a string", and their team knows immediately where to fix it.

**Key point:** Customer data should reach the agent only after it has passed through a schema.

## Three traps, and how to fix the string amount

The most common trap is calling `.parse` directly on raw customer data inside the agent loop without a try/catch. A single bad payload throws a `ZodError` and the whole run stops. At this entry point, `safeParse` is safer.

The next trap is coercing the value yourself to "make it work" without asking the customer. Turning `"12.5"` into 12.5 looks harmless, but you do not yet know whether it is a transient bug, a new format or a different unit. Silent coercion hides exactly the signal the schema has just caught.

And do not keep a hand-written `interface` running in parallel with a schema. Sooner or later the two will drift; `z.infer` brings them back to one source.

For the `amount` case there are two correct fixes, depending on the customer's answer. If the customer confirms `amount` must be a number, keep `z.number()`: `safeParse` rejects the payload, you log the path and pass the error to their team. If the customer confirms the endpoint now returns a string on purpose, record that in the schema and convert in one clearly visible place:

```ts
const OrderRaw = z.object({
  id: z.string(),
  amount: z.string(), // customer confirmed: amount is returned as a string
  status: z.string(),
});

const r = OrderRaw.safeParse(rawFromCustomerApi);
if (r.success) {
  const amount = Number(r.data.amount);
  if (Number.isNaN(amount)) {
    // string cannot be read as a number: reject, do not pass it to the agent
  } else {
    runAgent({ ...r.data, amount });
  }
}
```

The difference is that the conversion is now a decision agreed with the customer, commented and kept in one place in the code, rather than a silent patch nobody knows about.

## Where can Zod not help you?

Zod checks the shape of data, not its meaning. If `amount` is the number 1250, the schema accepts it whether the customer counts in cents or in whole currency units. To find that out, you have to sit down with the customer's engineers.

Nor does Zod decide for you what happens when data is wrong: reject it, log it and skip, or route it to a person. That is a product decision, and it should be discussed with the customer early. One more obvious limit: Zod lives in the TypeScript ecosystem, so if the customer's pipeline is written in another language, you can only use Zod in the TypeScript parts.

Then there is the question of versions. Zod 4 has been announced as stable. The release notes say Zod 4 parses up to 14.71 times faster than Zod 3 in their benchmarks, and that the core bundle is about 57% smaller (2.3x).

If the customer's codebase is still on Zod 3, treat the upgrade as a separate change with its own tests, and do not fold it into a bug-fix commit.

## What to learn first, and how to show the skill

A sensible order is `z.object` with a few basic types, then `safeParse`, then reading `ZodError`, and finally `z.infer`. Those four are enough to build an entry-point validation layer for most integrations. After that, read how the MCP TypeScript SDK uses Zod to describe tool inputs, because that is exactly where the schema meets the agent.

When reading FDE job descriptions, if a posting mentions building MCP servers or agent tools in TypeScript, mention your Zod experience in the cover letter: the MCP TypeScript SDK requires Zod, so this is a skill you will use from week one.

On your CV, do not just write "knows Zod". Write in terms of outcomes, for example: put schemas at the entry point for customer APIs, grouped errors by field, and shortened the time to find the root cause when the agent answered wrongly.

Many people think a good FDE is someone who writes good prompts. But once an agent goes into production at a customer site, what keeps it stable is often a schema that runs before the prompt.

**Try this week:**

- Pick a real endpoint your agent is calling, write a Zod schema for its payload and run safeParse over the 20 most recent responses in your logs.
- Group every ZodError by path, count which field fails most often, and bring that list to your next technical meeting with the customer.
- Replace one hand-written interface in the project with z.infer from a schema, so the type and the validation can no longer drift apart.

## Sources

- [Zod](https://zod.dev)

- [Basics | Zod](https://zod.dev/basics)

- [colinhacks/zod: TypeScript-first schema validation with static type inference](https://github.com/colinhacks/zod)

- [Release notes | Zod 4](https://zod.dev/v4)

- [MCP TypeScript SDK](https://ts.sdk.modelcontextprotocol.io)
