Zod 07 🛡️ Primitive Types — z.string(), z.number(), z.boolean(), z.date()
Zod’s primitive schemas are the atoms from which every larger schema is built. z.string(), z.number(), z.boolean(), and z.date() each produce a schema whose runtime validator checks the JavaScript type and whose inferred TypeScript type is the corresponding primitive. On their own, these schemas accept any value of that type. Constraints—.min(), .max(), .email(), .int()—narrow the accepted set without changing the inferred type. Zod v4 moved several string-format validators to top-level functions, so the syntax for email, URL, and UUID checks differs from v3 .
This chapter covers the four core primitives, their constraint methods, and the version differences that determine which syntax is current. It explains why .email() is no longer a string method in v4, how number constraints compose, and why z.date() validates a Date object but does not coerce a string into one. The sections that follow walk through strings and their format validators, numbers and their numeric constraints, booleans and dates, and the coercion methods that bridge types.
Key point: Primitives validate the JavaScript type first, then apply constraints; in Zod v4, string-format checks like email and UUID moved to top-level functions such as z.email() and z.uuid(), while numeric constraints remain chained methods.
Why primitive schemas exist
The type gate problem. Every schema performs two jobs: it checks the runtime type and it narrows the accepted values. z.string() alone checks only that the value is a string; it accepts "", a megabyte of text, and every character in between . The constraint methods are what make the schema useful: .min(1) rejects the empty string, .email() rejects anything that is not an email address, .max(255) enforces a database column limit.
The v3-to-v4 syntax problem. Zod v4 promoted string-format validators to top-level functions. z.string().email() became z.email(), z.string().uuid() became z.uuid(), and z.string().url() became z.url() . The methods still exist for compatibility in some builds, but the v4 documentation and migration guides treat the top-level form as the correct syntax. A codebase that mixes the two forms is neither fully v3 nor fully v4, and the type inference may not match expectations.
The date coercion problem. z.date() validates that the input is already a JavaScript Date object. It does not parse strings or numbers. JSON has no date type, so an API response arrives with a string, and z.date() rejects it. z.coerce.date() exists for that boundary: it passes the input to new Date() and validates the result . Using z.date() on a string input produces a validation error that looks like a type mismatch but is actually a missing coercion step.
a. Strings and format validators
z.string() accepts any string. Constraints narrow the set: .min(n), .max(n), .length(n), .regex(), .startsWith(), .endsWith(), .trim(), .toLowerCase(). The inferred type remains string regardless of which constraints are applied.
import { z } from "zod";
// Plain string: accepts anything
const AnyString = z.string(); // string
// Constrained string: same type, narrower acceptance
const NameSchema = z.string().min(1).max(100); // string
const CodeSchema = z.string().length(36); // string (UUID length)
const RegexSchema = z.string().regex(/^[A-Z]+$/); // string
In Zod v4, format validators are top-level functions. The v3 chained methods are deprecated in favor of the new form .
// Zod v4: top-level format validators
const EmailSchema = z.email(); // string, must be valid email
const UrlSchema = z.url(); // string, must be valid URL
const UuidSchema = z.uuid(); // string, must be valid UUID
const IsoDateSchema = z.iso.date(); // string, must be ISO date format
A v3 codebase that writes z.string().email() will still compile in some v4 builds, but the canonical form is z.email(). The migration is mechanical: replace the chained method with the top-level function and remove the z.string() prefix.
b. Numbers and numeric constraints
z.number() accepts any JavaScript number, including NaN, Infinity, and -Infinity. Constraints narrow the set: .min(n), .max(n), .int(), .positive(), .nonnegative(), .negative(), .nonpositive(), .multipleOf(n), .finite() . The inferred type remains number.
// Plain number: accepts any numeric value
const AnyNumber = z.number(); // number
// Constrained numbers: same type, narrower acceptance
const AgeSchema = z.number().int().positive(); // number, positive integer
const PortSchema = z.number().int().min(1).max(65535); // number, valid port
const RatioSchema = z.number().min(0).max(1); // number, 0 to 1
const StepSchema = z.number().multipleOf(5); // number, multiple of 5
The NaN problem is worth noting: z.number() accepts NaN because typeof NaN === "number". A schema that must reject NaN needs .finite() or a .refine() check. The .int() method rejects non-integers but not NaN; .finite() rejects both NaN and Infinity .
c. Booleans and dates
z.boolean() accepts only true and false. There are no constraints to apply; the type is the constraint.
const FlagSchema = z.boolean(); // boolean
z.date() accepts only JavaScript Date objects. It does not parse strings. The schema validates that the object is a Date and that it is not an invalid date (new Date("invalid")).
const TimestampSchema = z.date(); // Date
// Valid
TimestampSchema.parse(new Date());
// Invalid: string is not a Date
TimestampSchema.parse("2026-05-05"); // ZodError
For inputs that arrive as strings or numbers, z.coerce.date() passes the input through new Date() before validation . This is the correct schema for an API boundary where the JSON contains an ISO string.
// Coercion: accepts string or number, returns Date
const CoercedDate = z.coerce.date(); // input: unknown, output: Date
CoercedDate.parse("2026-05-05T10:00:00Z"); // Date object
CoercedDate.parse(1746439200000); // Date object
Complete Example Session
// ============================================
// PART 1: STRINGS AND CONSTRAINTS
// ============================================
import { z } from "zod";
const NameSchema = z.string().min(1, "Name is required").max(100);
const EmailSchema = z.email("Enter a valid email address"); // v4 top-level
const UuidSchema = z.uuid(); // v4 top-level
// ============================================
// PART 2: NUMBERS AND CONSTRAINTS
// ============================================
const AgeSchema = z.number().int().positive().max(150);
const PortSchema = z.number().int().min(1).max(65535);
const RatioSchema = z.number().finite().min(0).max(1);
// ============================================
// PART 3: BOOLEANS
// ============================================
const EnabledSchema = z.boolean();
// ============================================
// PART 4: DATES
// ============================================
// Strict: input must already be a Date
const StrictDateSchema = z.date();
// Coerced: string or number becomes Date
const CoercedDateSchema = z.coerce.date();
// ============================================
// PART 5: THE INFERRED TYPES
// ============================================
type Name = z.infer<typeof NameSchema>; // string
type Age = z.infer<typeof AgeSchema>; // number
type Enabled = z.infer<typeof EnabledSchema>; // boolean
type StrictDate = z.infer<typeof StrictDateSchema>; // Date
type CoercedInput = z.input<typeof CoercedDateSchema>; // unknown
type CoercedOutput = z.output<typeof CoercedDateSchema>; // Date
// ============================================
// PART 6: PARSING BEHAVIOR
// ============================================
NameSchema.parse("Ana"); // valid
NameSchema.parse(""); // ZodError: too_small
AgeSchema.parse(30); // valid
AgeSchema.parse(30.5); // ZodError: expected int
AgeSchema.parse(-5); // ZodError: too_small
StrictDateSchema.parse(new Date()); // valid
StrictDateSchema.parse("2026-05-05"); // ZodError: invalid_type
CoercedDateSchema.parse("2026-05-05"); // Date object
The parts covered string constraints and v4 format validators, number constraints, boolean validation, the strict versus coerced date distinction, the inferred input/output types, and parsing behavior for each primitive.
Quick Reference
Core Primitives
| Schema | Inferred Type | Accepts |
|---|---|---|
z.string() | string | Any string |
z.number() | number | Any number (incl. NaN) |
z.boolean() | boolean | true / false |
z.date() | Date | Valid Date object |
String Validators (v4)
| Validator | Syntax | Checks |
|---|---|---|
| Min length | .min(n) | Length ≥ n |
| Max length | .max(n) | Length ≤ n |
| Exact length | .length(n) | Length = n |
| Regex | .regex(re) | Pattern match |
z.email() (v4) | Email format | |
| URL | z.url() (v4) | URL format |
| UUID | z.uuid() (v4) | UUID format |
Number Constraints
| Constraint | Effect |
|---|---|
.int() | Integer only |
.positive() | > 0 |
.nonnegative() | ≥ 0 |
.negative() | < 0 |
.min(n) / .max(n) | Range |
.multipleOf(n) | Divisibility |
.finite() | Rejects NaN/Infinity |
Date Schemas
| Schema | Input | Output |
|---|---|---|
z.date() | Date | Date |
z.coerce.date() | unknown | Date |
Best Practices
✅ Do This:
// Apply constraints at definition
z.string().min(1).max(255) // ✅
// Use v4 top-level format validators
z.email() // ✅
// Use coercion for API date strings
z.coerce.date() // ✅
// Reject NaN explicitly when it matters
z.number().finite() // ✅
❌ Don’t Do This:
// Plain string with no constraints where data requires them
z.string() // accepts "" // ❌
// v3 chained email in v4 codebase
z.string().email() // ❌ (use z.email())
// z.date() on JSON string input
z.date().parse("2026-05-05") // ❌ (type error)
// Assume z.number() rejects NaN
z.number().parse(NaN) // passes // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Empty string accepted | z.string() has no length check | Add .min(1) |
NaN passes number validation | typeof NaN === "number" | Add .finite() |
| Date string rejected | z.date() requires Date object | Use z.coerce.date() |
| v3 syntax fails in v4 | Format validators moved top-level | z.email() not z.string().email() |
| UUID check too loose in v3 | v4 UUID is strict RFC | Verify format expectations |
Real-World Examples
1. Constrained string
z.string().min(1).max(100);
2. Email (v4)
z.email();
3. UUID (v4)
z.uuid();
4. Positive integer
z.number().int().positive();
5. Port range
z.number().int().min(1).max(65535);
6. Finite number
z.number().finite();
7. Boolean flag
z.boolean();
8. Strict date
z.date();
9. Coerced date
z.coerce.date();
10. ISO date string (v4)
z.iso.date();
Visual
Primitive Schema Anatomy
┌─────────────────────────────────────────────────────────┐
│ z.string().min(1).max(100) │
│ │
│ z.string() ──► runtime type check: typeof === string │
│ │ │
│ ▼ │
│ .min(1) ──► length >= 1 │
│ │ │
│ ▼ │
│ .max(100) ──► length <= 100 │
│ │ │
│ ▼ │
│ inferred type: string (unchanged) │
│ │
│ Constraints narrow acceptance, not type │
└─────────────────────────────────────────────────────────┘
v3 vs v4 String Formats
┌─────────────────────────────────────────────────────────┐
│ ZOD v3 ZOD v4 │
│ │
│ z.string().email() z.email() │
│ z.string().url() z.url() │
│ z.string().uuid() z.uuid() │
│ │
│ Chained method Top-level function │
│ Deprecated in v4 Canonical in v4 │
└─────────────────────────────────────────────────────────┘
Date Coercion Boundary
┌─────────────────────────────────────────────────────────┐
│ JSON API RESPONSE │
│ │
│ { "created": "2026-05-05T10:00:00Z" } │
│ │ │
│ ▼ │
│ z.date().parse(...) │
│ │ │
│ ▼ │
│ ZodError: expected Date, received string │
│ │
│ z.coerce.date().parse(...) │
│ │ │
│ ▼ │
│ new Date("2026-05-05T10:00:00Z") │
│ │ │
│ ▼ │
│ Valid Date object │
└─────────────────────────────────────────────────────────┘
Number Constraint Composition
┌─────────────────────────────────────────────────────────┐
│ z.number().int().positive().max(150) │
│ │
│ z.number() ──► typeof === number │
│ │ │
│ ▼ │
│ .int() ──► no decimal part │
│ │ │
│ ▼ │
│ .positive() ──► > 0 │
│ │ │
│ ▼ │
│ .max(150) ──► <= 150 │
│ │
│ Each constraint adds a check; type remains number │
└─────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| String primitive | z.string() → string |
| Number primitive | z.number() → number |
| Boolean primitive | z.boolean() → boolean |
| Date primitive | z.date() → Date |
| Email (v4) | z.email() |
| UUID (v4) | z.uuid() |
| Date coercion | z.coerce.date() |
| NaN rejection | .finite() |
| Constraint effect | Narrows acceptance, type unchanged |
Key takeaways:
- Primitives validate the JavaScript type first.
z.string()checkstypeof === "string";z.number()checkstypeof === "number"; constraints are additional checks layered on top. - Zod v4 moved string formats to top-level functions.
z.email(),z.url(), andz.uuid()replace the v3 chained methods. The migration is mechanical but necessary for consistent v4 syntax . z.number()acceptsNaN. This is a JavaScript consequence, not a Zod bug. Use.finite()whenNaNandInfinitymust be rejected .z.date()requires aDateobject. JSON strings fail validation.z.coerce.date()passes the input throughnew Date()and is the correct schema for API boundaries .- Constraints do not change the inferred type.
z.string().min(1)still infers asstring; the length check is runtime-only.
Remember: A primitive schema without constraints is a type gate, not a validation rule. z.string() accepts the empty string, z.number() accepts NaN, and z.date() rejects the string that JSON actually sends. The constraints are the validation, and the coercion is the bridge across the JSON boundary. In v4, check the syntax: z.email() not z.string().email(), z.coerce.date() not z.date() when the input is a string. The primitive is the atom; the constraints are the chemistry.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!