| | |

Zod 07 🛡️ Primitive Types — z.string(), z.number(), z.boolean(), z.date()

⏱️ Estimated read: 9 min | |

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

SchemaInferred TypeAccepts
z.string()stringAny string
z.number()numberAny number (incl. NaN)
z.boolean()booleantrue / false
z.date()DateValid Date object

String Validators (v4)

ValidatorSyntaxChecks
Min length.min(n)Length ≥ n
Max length.max(n)Length ≤ n
Exact length.length(n)Length = n
Regex.regex(re)Pattern match
Emailz.email() (v4)Email format
URLz.url() (v4)URL format
UUIDz.uuid() (v4)UUID format

Number Constraints

ConstraintEffect
.int()Integer only
.positive()> 0
.nonnegative()≥ 0
.negative()< 0
.min(n) / .max(n)Range
.multipleOf(n)Divisibility
.finite()Rejects NaN/Infinity

Date Schemas

SchemaInputOutput
z.date()DateDate
z.coerce.date()unknownDate

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

PitfallWhy It HappensFix
Empty string acceptedz.string() has no length checkAdd .min(1)
NaN passes number validationtypeof NaN === "number"Add .finite()
Date string rejectedz.date() requires Date objectUse z.coerce.date()
v3 syntax fails in v4Format validators moved top-levelz.email() not z.string().email()
UUID check too loose in v3v4 UUID is strict RFCVerify 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

ItemValue
String primitivez.string() → string
Number primitivez.number() → number
Boolean primitivez.boolean() → boolean
Date primitivez.date() → Date
Email (v4)z.email()
UUID (v4)z.uuid()
Date coercionz.coerce.date()
NaN rejection.finite()
Constraint effectNarrows acceptance, type unchanged

Key takeaways:

  • Primitives validate the JavaScript type first. z.string() checks typeof === "string"; z.number() checks typeof === "number"; constraints are additional checks layered on top.
  • Zod v4 moved string formats to top-level functions. z.email(), z.url(), and z.uuid() replace the v3 chained methods. The migration is mechanical but necessary for consistent v4 syntax .
  • z.number() accepts NaN. This is a JavaScript consequence, not a Zod bug. Use .finite() when NaN and Infinity must be rejected .
  • z.date() requires a Date object. JSON strings fail validation. z.coerce.date() passes the input through new Date() and is the correct schema for API boundaries .
  • Constraints do not change the inferred type. z.string().min(1) still infers as string; 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.

Was this helpful?


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!