CodingNic

Input Validation

Zod Schemas

Input Validation 15 min read

Zod Schemas

Objectives

By the end of this lesson, you should be able to:

  • Define a Zod schema with typed, constrained fields
  • Validate data against a schema, and read structured error output
  • Explain why Zod strips unrecognized fields by default

💡 Why this matters: Zod is what actually replaces the manual if checks from the last lesson, a single schema expressing every rule a field needs to satisfy, checked in one call.

⚠️ A note on verification: every snippet and every output in this lesson was actually run.

Installing Zod

bash
npm install zod

Defining a Schema

javascript
const { z } = require('zod');

const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8, 'Password must be at least 8 characters'),
  age: z.number().int().min(13).optional()
});

Reading it field by field: email must be a string in valid email format, password must be a string of at least 8 characters (with a custom error message), age, if present at all, must be a whole number of at least 13, .optional() means it can be omitted entirely.

Validating Successfully

javascript
const result = registerSchema.safeParse({ email: 'erin@example.com', password: 'sunshine123', age: 25 });
console.log(result.success, result.data);
text
true { email: 'erin@example.com', password: 'sunshine123', age: 25 }

safeParse() never throws, it returns an object with success and either data (validated, and safe to use) or error (structured validation failures), the right choice for handling untrusted request input.

Validating a Bad Email

javascript
const badEmail = registerSchema.safeParse({ email: 'not-an-email', password: 'sunshine123' });
console.log(badEmail.success);
console.log(badEmail.error.issues.map(i => ({ path: i.path, message: i.message })));
text
false
[ { path: [ 'email' ], message: 'Invalid email address' } ]

Each issue names the exact field (path) and a specific, readable message, structured data, not a single opaque error string, exactly what Module 5’s later lesson uses to return clear, per-field errors to a client.

Validating a Short Password

javascript
const shortPassword = registerSchema.safeParse({ email: 'erin@example.com', password: 'short' });
console.log(shortPassword.error.issues.map(i => ({ path: i.path, message: i.message })));
text
[ { path: [ 'password' ], message: 'Password must be at least 8 characters' } ]

The custom message passed to .min(8, '...') shows up exactly as written, worth doing for any rule where the default message wouldn’t be clear to an end user.

Multiple Errors at Once

javascript
const missingFields = registerSchema.safeParse({});
console.log(missingFields.error.issues.map(i => ({ path: i.path, message: i.message })));
text
[
  { path: [ 'email' ], message: 'Invalid input: expected string, received undefined' },
  { path: [ 'password' ], message: 'Invalid input: expected string, received undefined' }
]

Zod reports every failing field at once, not just the first one, letting a client fix every problem in a single round trip instead of one error at a time.

Stripping Unrecognized Fields

javascript
const extraField = registerSchema.safeParse({ email: 'erin@example.com', password: 'sunshine123', isAdmin: true });
console.log(extraField.success, extraField.data);
text
true { email: 'erin@example.com', password: 'sunshine123' }

isAdmin: true, a field never declared in the schema, is silently dropped from data, not passed through. This is exactly the mass-assignment protection the last lesson’s problem needed, whatever wasn’t explicitly declared as a valid field never survives validation, whether it’s isAdmin, role, or anything else an attacker might try to sneak into a request body.

Try It

  1. Define a schema with at least three fields, each with a different kind of rule (a string format, a minimum length, a number range).
  2. Validate data that fails two rules at once, and confirm both errors appear in result.error.issues.
  3. Validate data with an extra, undeclared field, and confirm it’s stripped from result.data.
  4. Explain, in your own words, why safeParse() is the right choice for validating untrusted request data, compared to Zod’s parse(), which throws on failure instead.

Recap

  • A Zod schema declares a field’s type, format, and constraints in one place, safeParse() validates data against it without throwing.
  • Validation errors come back structured, one entry per failing field, with a specific path and message.
  • Fields not declared in the schema are stripped from validated output by default, closing the mass-assignment gap from the last lesson.

Next lesson: wiring Zod validation into Express as reusable middleware, with structured error responses.