Runtime Validation with Zod

Zod is a schema validation library that checks data at runtime — exactly where TypeScript's compile-time types can't reach — and lets you derive a static type from each schema so your validator and your types never drift apart.

Learn Runtime Validation with Zod in our free TypeScript course — an interactive lesson with runnable examples, a practice exercise and a quick reference.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

A TypeScript type is a blueprint ; Zod is the inspector at the door . The blueprint says what a valid visitor looks like, but it only exists on paper — once the building is open, anyone can walk up. Zod is the guard who actually checks each person against the blueprint and turns away anyone who doesn't match, so nothing malformed ever gets inside your program.

1. A Schema Is the Single Source of Truth

You describe the shape you expect once, with builders like z.object , z.string , and z.number . Then z.infer<typeof Schema> pulls a static TypeScript type straight out of that schema — so the validator and the type are always the same thing. This is real Zod syntax, so read it here rather than running it:

2. .parse() vs .safeParse()

Once you have a schema, you run data through it. .parse(value) returns the validated value or throws on bad data — great at a clear boundary with a try/catch. .safeParse(value) never throws: it hands back a result object ( {' '} ) so you can branch on failure. The box below uses a tiny stand-in for Zod so it runs anywhere.

3. Refinements, Optionals, Defaults & Coercion

Schemas chain modifiers: .optional() allows undefined , .nullable() allows null , .default(x) fills in a fallback, and z.coerce.number() converts before validating. For rules the built-ins can't express, .refine(fn, message) adds a custom predicate. We model each behaviour in plain JS below:

Your turn. Fill in the two blanks marked ___ to finish a non-empty-string validator, then run it and check the output.

4. Validating API Responses

This is where Zod earns its keep. A TypeScript interface for an API response is only a compile-time assumption — the server can still send something else. Run the response through a schema with .safeParse() and malformed data fails loudly at the boundary instead of silently corrupting state deep inside your app.

No blanks this time — just a brief and an outline. Build the validator yourself (default theme, coerced volume, range check), run it, and match your output to the example in the comments.

Practice quiz

What problem does Zod solve that TypeScript types alone cannot?

  • It validates data at runtime, where TS types are already erased
  • It makes the compiler faster
  • It replaces interfaces entirely
  • It adds new syntax to JavaScript

Answer: It validates data at runtime, where TS types are already erased. TypeScript types vanish at runtime. Zod validates real values as the program runs, catching bad data the compiler never sees.

Which Zod method throws an error when the value is invalid?

  • .check()
  • .safeParse()
  • .parse()
  • .validate()

Answer: .parse(). .parse(value) returns the typed value on success and throws a ZodError on failure.

What does .safeParse() return instead of throwing?

  • undefined on failure
  • A result object with success plus either data or error
  • Always the parsed value
  • A boolean only

Answer: A result object with success plus either data or error. .safeParse() returns { success: true, data } or { success: false, error }, so you handle failure without try/catch.

How do you derive a static TypeScript type from a Zod schema?

  • Schema as Type
  • typeof Schema
  • Schema.toType()
  • z.infer<typeof Schema>

Answer: z.infer<typeof Schema>. z.infer<typeof Schema> extracts the TypeScript type a schema describes, keeping types and validation in sync.

What does .refine() add to a schema?

  • A custom validation rule beyond the basic shape
  • A default value
  • A type coercion step
  • An optional flag

Answer: A custom validation rule beyond the basic shape. .refine(fn, message) attaches a custom predicate, letting you express rules like 'password and confirm must match'.

What does z.coerce.number() do with the input "42"?

  • Leaves it as the string "42"
  • Converts it to the number 42 before validating
  • Returns NaN
  • Rejects it because it is a string

Answer: Converts it to the number 42 before validating. z.coerce.number() runs Number() on the input first, so the string "42" becomes the number 42.

Which schema accepts a string OR the value undefined?

  • z.string().nullable()
  • z.string().default()
  • z.string().required()
  • z.string().optional()

Answer: z.string().optional(). .optional() allows the value to be undefined; .nullable() allows null instead.

What is the best way to model a field that is exactly "admin" or "user"?

  • z.string()

z.enum([...]) restricts a string field to a fixed set of literal values and infers the union type.

Why validate an API response with Zod even though you have a TypeScript interface for it?

  • TypeScript cannot describe JSON
  • Interfaces are slower than Zod
  • The interface is only a compile-time promise; the network can return anything at runtime
  • Zod replaces fetch

Answer: The interface is only a compile-time promise; the network can return anything at runtime. An interface is just a compile-time assumption. Zod actually checks the bytes that arrive, so malformed responses fail loudly instead of corrupting your app.

What does z.string().default("guest") produce when the input is undefined?

  • null
  • The string "guest"
  • undefined
  • A validation error

Answer: The string "guest". .default(value) supplies the fallback when the input is undefined, so a missing field becomes "guest".