hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Validation for beginners with Zod

What is Zod

  • The problem with untyped data
  • Your first schema
  • Project setup

Primitive types

  • Strings
  • Numbers
  • Booleans, dates, and literals

Objects and arrays

  • Objects
  • Nested objects
  • Arrays
  • Unions and discriminated unions

Transforms and refinements

  • Transforms
  • Refinements
  • Preprocessing and coercion

Composing schemas

  • Pick, omit, and extend
  • Merge and intersection
  • Reusable schemas

Zod in practice

  • Validating API requests
  • Error formatting
  • Inferring TypeScript types
  • Cheatsheet and capstone

Objects

The building block of APIs

Every JSON API sends and receives objects. A contact submission is an object. A user profile is an object. A list of search results is an object containing an array. We have been using z.object() since the first lesson, but there is a lot more to it than just grouping fields together.

This lesson covers the details: required vs optional fields, default values, how Zod handles extra fields, and how to make entire schemas optional or required in one step.

Every field is required by default

import { z } from "zod/v4";

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive(),
});

Every field inside z.object() is required unless you explicitly make it optional. If a field is missing, validation fails:

UserSchema.parse({ name: "Alice", email: "[email protected]", age: 30 }); // passes
UserSchema.parse({ name: "Alice", email: "[email protected]" }); // fails: age is required
UserSchema.parse({ name: "Alice" }); // fails: email and age required

This is a good default. You want to be explicit about which fields are optional rather than accidentally allowing missing data.

Making fields optional

You already know .optional() from the previous lesson. In the context of objects, it means “this field does not have to be present”:

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive().optional(), // can be omitted
  bio: z.string().max(500).optional(), // can be omitted
});

UserSchema.parse({ name: "Alice", email: "[email protected]" }); // passes: age and bio omitted

Default values

What if you want a field to have a value even when the client does not send it? That is what .default() does:

const SettingsSchema = z.object({
  theme: z.enum(["light", "dark"]).default("light"),
  pageSize: z.number().int().min(1).max(100).default(20),
  notifications: z.boolean().default(true),
});

SettingsSchema.parse({});
// returns { theme: "light", pageSize: 20, notifications: true }

SettingsSchema.parse({ theme: "dark" });
// returns { theme: "dark", pageSize: 20, notifications: true }

Default values fill in anything that is missing. But if the field is provided, it still gets validated. { pageSize: 200 } fails because 200 exceeds max(100). The default only kicks in when the field is absent, not when it is invalid.

This is the difference between .optional() and .default(). With .optional(), a missing field stays undefined. With .default(), a missing field gets a real value. If your code cannot handle undefined, use .default().

Extra fields: strip, strict, passthrough

What happens when the client sends fields that are not in your schema? By default, Zod strips them:

const schema = z.object({ name: z.string() });

schema.parse({ name: "Alice", extra: "ignored" });
// returns { name: "Alice" } — "extra" is removed

This is usually what you want for API input. The client might send extra fields (maybe a newer version of the frontend sends fields your backend does not know about yet), and silently dropping them is safe.

But sometimes you want different behavior. If you want to reject extra fields, use .strict():

const schema = z.object({ name: z.string() }).strict();

schema.parse({ name: "Alice", extra: "rejected" });
// throws: "Unrecognized key(s) in object: 'extra'"

And if you want to keep extra fields, use .passthrough():

const schema = z.object({ name: z.string() }).passthrough();

schema.parse({ name: "Alice", extra: "kept" });
// returns { name: "Alice", extra: "kept" }

For most API work, the default stripping behavior is what you want. Use .strict() when you need to be defensive about unexpected data. Use .passthrough() when you are proxying data and need to forward unknown fields.

partial() and required()

Here is a pattern that comes up constantly in APIs. You have a “create” endpoint where all fields are required, and an “update” endpoint where the client only sends the fields they want to change. You could write two separate schemas, but .partial() does it for you:

const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number(),
});

const PartialUser = UserSchema.partial();
// All fields are now optional: { name?: string, email?: string, age?: number }

PartialUser.parse({}); // passes: everything is optional

And .required() goes the other direction, making everything required:

const RequiredUser = PartialUser.required();
// All fields are required again

The create schema requires all fields. The update schema uses .partial() so the client only sends what changed. One source of truth, two schemas. We will use this pattern heavily when we get to schema composition.

Next up: nested objects. Real-world data is rarely flat. Addresses live inside orders, preferences live inside users, and Zod handles all of it.

Exercises

Exercise 1: Create a schema with 3 required fields and 2 optional fields. Test with various combinations.

Exercise 2: Add .default() to a field. Parse an object without that field. Verify the default is used.

Exercise 3: Parse an object with extra fields using default (stripped), .strict() (rejected), and .passthrough() (kept). Compare the results.

What does Zod do with extra fields that are not in the schema?

← Booleans, dates, and literals Nested objects →

© 2026 hectoday. All rights reserved.