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
ifchecks 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
npm install zod
Defining a Schema
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
const result = registerSchema.safeParse({ email: 'erin@example.com', password: 'sunshine123', age: 25 });
console.log(result.success, result.data);
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
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 })));
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
const shortPassword = registerSchema.safeParse({ email: 'erin@example.com', password: 'short' });
console.log(shortPassword.error.issues.map(i => ({ path: i.path, message: i.message })));
[ { 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
const missingFields = registerSchema.safeParse({});
console.log(missingFields.error.issues.map(i => ({ path: i.path, message: i.message })));
[
{ 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
const extraField = registerSchema.safeParse({ email: 'erin@example.com', password: 'sunshine123', isAdmin: true });
console.log(extraField.success, extraField.data);
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
- Define a schema with at least three fields, each with a different kind of rule (a string format, a minimum length, a number range).
- Validate data that fails two rules at once, and confirm both errors appear in
result.error.issues. - Validate data with an extra, undeclared field, and confirm it’s stripped from
result.data. - Explain, in your own words, why
safeParse()is the right choice for validating untrusted request data, compared to Zod’sparse(), 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.