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()?