Zod fundamentals
Three bugs so far, all with the same shape. A value arrived, the code used it, and nobody checked it first.
Fixing each one where it lands works, and it's endless. Every new destination is a new place to remember.
The other move is to check the value once, as it arrives, against a description of what you were expecting. That description is a schema, and Zod is the library this section uses to write one.
What a schema is
Picture the scanner at an airport. Your bag goes in one end. The machine has been set up with rules: nothing sharp, no liquids over a certain size. A bag that satisfies the rules comes out the other side unchanged. A bag that doesn't never makes it through.
A schema is the machine's settings. Parsing is running the bag through.
Install it and bring it in:
npm install zodimport * as z from 'zod'The simplest schema is a single rule about a single value:
const teacherSchema = z.string()
const teacher = 'Jonathan'
console.log(teacherSchema.parse(teacher))
// 'Jonathan'parse hands the data to the machine. The data satisfies the rule, so it comes back out.
Give it something that doesn't fit and the machine stops:
teacherSchema.parse(12345)
// ZodError: Invalid input: expected string, received numberThat's the whole model. Describe what's acceptable, run values through, get the value back or get an error.
The schema is the description of what you'll accept. Parsing is the act of checking something against it. You write the schema once and parse with it as often as you like.
Describing an object
A teacher is rarely one string. Give the schema a shape instead:
const teacherSchema = z.object({
name: z.string(),
age: z.number(),
})
const teacher = {
name: 'Jonathan',
age: 21,
}
console.log(teacherSchema.parse(teacher))
// { name: 'Jonathan', age: 21 }Each entry inside z.object() is a key, and its value is the schema for that key. The machine now expects an object with a name that is any string and an age that is any number.
Break one of them and the error says exactly which:
teacherSchema.parse({ name: 'Jonathan', age: '21' })
// ZodError: Invalid input: expected number, received stringZod ships the primitives you'd expect, z.string(), z.number(), z.boolean(), and objects nest inside objects as deeply as your data does.
So the same idea covers a two-field form and a deeply structured API response. You're always describing one level at a time.
Why the check has to happen at runtime
TypeScript describes shapes too:
type Teacher = {
name: string
age: number
}That looks like the schema and does a completely different job. TypeScript checks types while you write code. When it compiles to JavaScript, every annotation is erased, so nothing survives into the running program.
Which is fine for values your own code produced, and useless for values it didn't. A request body, an API response, a form submission: those arrive while the program is running, long after the types stopped existing. TypeScript assumes your data is correct. Zod checks.
A schema is a description of a type that still exists when the data shows up.
TypeScript is a conversation with you while you write. The schema is a conversation with the data while the program runs.
Try it
Write it from scratch, without copying the teacher example.
- Import Zod.
- Create a
characterSchemawith two keys:name, a string, andepisode, a number. - Define a
characterobject with the name'Luke Skywalker'and episode4. - Validate the character against the schema and log the result.
- Change
episodeto the string'4'and predict what happens before you run it.
Compare your answers
import * as z from 'zod'
const characterSchema = z.object({
name: z.string(),
episode: z.number(),
})
const character = {
name: 'Luke Skywalker',
episode: 4,
}
console.log(characterSchema.parse(character))
// { name: 'Luke Skywalker', episode: 4 }Two keys means z.object() rather than a bare primitive, and each key gets its own schema.
With episode as '4', parse throws ZodError: Invalid input: expected number, received string. The string '4' is not a number, and Zod will not quietly convert it for you. Converting on purpose is a separate instruction, which the next chapter covers.
Episode 4 is correct, incidentally. Luke Skywalker first appears in the original 1977 Star Wars film, which was later numbered Episode IV.
Where this goes next
Right now a schema does one thing: it accepts a value or throws. That's enough to describe data and not yet enough to build with.
Inferring types and coercing input adds the two pieces that make it practical. One schema can hand TypeScript the type as well, so the shape is written once. And a failed parse can hand you a result to inspect instead of an exception to catch.

