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

Booleans, dates, and literals

The remaining primitives

We have covered strings and numbers in detail. Now let’s round out the primitive types with booleans, dates, literals, and enums. These are simpler individually, but they unlock powerful patterns when combined, especially enums and literals.

Booleans

z.boolean() validates that a value is true or false:

import { z } from "zod/v4";

z.boolean().parse(true); // returns true
z.boolean().parse(false); // returns false
z.boolean().parse("true"); // fails: "Expected boolean, received string"
z.boolean().parse(1); // fails: "Expected boolean, received number"

Same strictness as numbers. "true" is not a boolean. 1 is not a boolean. If you need to accept truthy/falsy values like these, coercion handles that, but we will get there later.

Where do booleans show up? Consent checkboxes, toggles, feature flags:

const PreferencesSchema = z.object({
  newsletter: z.boolean(),
  darkMode: z.boolean(),
});

Dates

z.date() validates that a value is a JavaScript Date object:

z.date().parse(new Date()); // passes
z.date().parse("2024-01-15"); // fails: "Expected date, received string"
z.date().parse(new Date("invalid")); // fails: Invalid Date is rejected

Here is something that trips people up: dates that come from JSON are always strings. JSON has no Date type. So if an API sends you "2024-01-15T10:30:00Z", that is a string, not a Date object. You need z.coerce.date() or a transform to convert it (we will cover both later).

You can also add constraints to dates:

z.date().min(new Date("2024-01-01")); // must be on or after Jan 1, 2024
z.date().max(new Date()); // must be in the past

[!NOTE] Most APIs store dates as ISO 8601 strings. For validating date strings without converting to Date objects, use z.string().datetime() from the Strings lesson.

Literals

Now things get more interesting. z.literal() validates that a value is exactly one specific value:

z.literal("active").parse("active"); // passes
z.literal("active").parse("inactive"); // fails
z.literal(42).parse(42); // passes
z.literal(42).parse(43); // fails
z.literal(true).parse(true); // passes
z.literal(true).parse(false); // fails

Why would you want a schema that only accepts one specific value? On their own, literals seem pointless. But they become powerful when combined with unions. Imagine a payment system where each payment method has a different shape. A method: z.literal("credit_card") field tells Zod which shape to expect. We will build this exact pattern in the unions lesson.

Enums

z.enum() validates that a value is one of a predefined set of strings:

const StatusSchema = z.enum(["active", "inactive", "pending"]);

StatusSchema.parse("active"); // returns "active"
StatusSchema.parse("deleted"); // fails: "Invalid enum value"
StatusSchema.parse("ACTIVE"); // fails: case-sensitive

Think of enums as multiple literals combined. Instead of writing z.union([z.literal("active"), z.literal("inactive"), z.literal("pending")]), you just write z.enum(["active", "inactive", "pending"]). Much cleaner.

You can also extract the list of allowed values, which is handy for documentation or dropdowns:

StatusSchema.options; // ["active", "inactive", "pending"]

Applying to the contact form

Remember the subject field in our database? It defaults to "general", but we should only allow specific subjects. Enums are perfect for this:

const SubjectSchema = z.enum(["general", "support", "sales", "feedback"]);

const ContactSchema = z.object({
  name: z.string().trim().min(1).max(100),
  email: z.string().trim().toLowerCase().email(),
  subject: SubjectSchema,
  message: z.string().trim().min(10).max(5000),
});

If the client sends subject: "complaint", Zod rejects it. Only the four predefined subjects are valid. This is exactly how production apps handle constrained fields like categories, roles, and statuses.

Nullable and optional

Two modifiers that work with any schema, not just primitives:

z.string().optional(); // string | undefined — the field can be missing
z.string().nullable(); // string | null — the field can be null

z.string().optional().parse(undefined); // returns undefined
z.string().optional().parse("hello"); // returns "hello"

z.string().nullable().parse(null); // returns null
z.string().nullable().parse("hello"); // returns "hello"

What is the difference? .optional() accepts undefined, meaning the field does not have to be present at all. .nullable() accepts null, meaning the field is present but explicitly set to “no value.” In JSON, a missing field is undefined. A field set to null is null. They are different concepts.

Our contact form’s phone field is optional. Users can skip it entirely:

const ContactSchema = z.object({
  name: z.string().trim().min(1).max(100),
  email: z.string().trim().toLowerCase().email(),
  phone: z.string().trim().min(7).max(20).optional(), // can be omitted
  subject: z.enum(["general", "support", "sales", "feedback"]),
  message: z.string().trim().min(10).max(5000),
});

With that, we have covered all the primitive types you need. In the next section, we will move on to objects and arrays, where things start to get really practical.

Exercises

Exercise 1: Create a schema for user preferences with boolean fields. Test with true, false, "true", and 1.

Exercise 2: Create an enum schema for T-shirt sizes: “xs”, “s”, “m”, “l”, “xl”. Test with valid and invalid values.

Exercise 3: Add .optional() to a string field. Parse undefined, null, "hello", and "". Which pass?

What is the difference between .optional() and .nullable()?

← Numbers Objects →

© 2026 hectoday. All rights reserved.