2. Primitives, Literals & String Formats

Zod v4.6.7 provides a comprehensive suite of scalar primitives, fixed-width numeric schemas, multi-value literals, enums, template literals, and first-class string format validators. One of the most impactful improvements in Zod v4 is the promotion of string formats (z.email(), z.uuid(), z.url(), z.jwt(), z.ipv4(), z.iso.datetime()) and fixed-width numbers (z.int(), z.float32(), z.int64()) to top-level tree-shakeable constructors.

02_primitives_and_formats_1
Zod v4.6.7 Primitives, Fixed-Width Numbers & Top-Level String Format Taxonomy
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.smartarts import CardList, Panel
from drawlib.styles import Styles

setup(width=168, height=84)

left_panel = Panel(
    "Scalar Primitives, Literals & Enums",
    "Core Types",
    style=Styles.Neutral,
    header_style=Styles.PrimaryNeutral.patch(shape_line_width=1.2),
    text_style=Styles.DarkBold.patch(text_size=11.0),
    description_style=Styles.Muted.patch(text_size=10.0),
    icon=phosphor.cube,
    icon_style=Styles.PrimaryBold,
    header_variant="filled",
    header_height=9.0,
)
left_panel.draw((5.0, 5.0), width=77.0, height=74.0, align="bottom_left")
l_xy, l_w, l_h = left_panel.get_content_box((5.0, 5.0), width=77.0, height=74.0, padding=(3.0, 2.5))

left_cards = CardList(
    style=Styles.White,
    text_style=Styles.DarkBold.patch(text_size=10.5),
    description_style=Styles.Muted.patch(text_size=10.0),
    badge_shape="rounded_rect",
    badge_style=Styles.PrimaryNeutral,
    badge_text_style=Styles.DarkBold.patch(text_size=10.0),
    spacing=2.0,
)
left_cards.add(
    "Numbers & Fixed-Width Integers",
    "z.number(), z.int(), z.int32(), z.uint32(),\nz.float32(), z.float64(), z.bigint(), z.int64()",
    badge_text="NUM",
    style=Styles.PrimaryFlat,
    text_style=Styles.WhiteBold.patch(text_size=10.5),
    description_style=Styles.White.patch(text_size=10.0),
    badge_style=Styles.White,
    badge_text_style=Styles.PrimaryBold.patch(text_size=10.0),
)
left_cards.add(
    "Strings & Template Literals",
    "z.string(), z.templateLiteral([...]),\n.min(), .max(), .regex(), .trim(), .slugify()",
    badge_text="STR",
)
left_cards.add(
    "Literals, Enums & Booleans",
    "z.literal([...]), z.enum([...]), z.boolean(),\nz.stringbool(), z.date(), z.symbol()",
    badge_text="LIT",
)
left_cards.add(
    "Nullish & Special Bounds",
    "z.null(), z.undefined(), z.void(),\nz.any(), z.unknown(), z.never(), z.nan()",
    badge_text="TOP",
)
left_cards.draw(l_xy, width=l_w, height=l_h)

right_panel = Panel(
    "Top-Level String & ISO Formats",
    "v4 Formats",
    style=Styles.Neutral,
    header_style=Styles.SecondaryNeutral.patch(shape_line_width=1.2),
    text_style=Styles.DarkBold.patch(text_size=11.0),
    description_style=Styles.Muted.patch(text_size=10.0),
    icon=phosphor.check_circle,
    icon_style=Styles.DarkBold,
    header_variant="filled",
    header_height=9.0,
)
right_panel.draw((86.0, 5.0), width=77.0, height=74.0, align="bottom_left")
r_xy, r_w, r_h = right_panel.get_content_box((86.0, 5.0), width=77.0, height=74.0, padding=(3.0, 2.5))

right_cards = CardList(
    style=Styles.White,
    text_style=Styles.DarkBold.patch(text_size=10.5),
    description_style=Styles.Muted.patch(text_size=10.0),
    badge_shape="rounded_rect",
    badge_style=Styles.SecondaryNeutral,
    badge_text_style=Styles.DarkBold.patch(text_size=10.0),
    spacing=2.0,
)
right_cards.add(
    "Web & Unique Identifiers",
    "z.email(), z.url(), z.httpUrl(), z.uuid(),\nz.uuidv4(), z.uuidv7(), z.cuid2(), z.ulid(), z.nanoid()",
    badge_text="WEB",
    badge_style=Styles.PrimaryNeutral,
)
right_cards.add(
    "Tokens, Encodings & Hashes",
    "z.jwt(), z.base64(), z.base64url(),\nz.hex(), z.hash('sha256'), z.emoji()",
    badge_text="SEC",
)
right_cards.add(
    "Network & Telecom Formats",
    "z.ipv4(), z.ipv6(), z.cidrv4(), z.cidrv6(),\nz.mac(), z.hostname(), z.e164()",
    badge_text="NET",
    badge_style=Styles.PrimaryNeutral,
)
right_cards.add(
    "ISO 8601 Date & Time (z.iso.*)",
    "z.iso.datetime(), z.iso.date(),\nz.iso.time(), z.iso.duration()",
    badge_text="ISO",
)
right_cards.draw(r_xy, width=r_w, height=r_h)

save()

1. Strings & Built-In String Transforms

z.string() validates that an input is a string and exposes chainable length checks, substring assertions, and in-place string normalization transforms:

import { z } from "zod";

const UsernameSchema = z
  .string({ error: "Username must be a string" })
  .trim() // Strips leading and trailing whitespace
  .toLowerCase() // Normalizes casing
  .min(3, "Must be at least 3 characters")
  .max(24, "Must be at most 24 characters")
  .regex(/^[a-z0-9_]+$/, "Only lowercase letters, digits, and underscores allowed");

// Additional string checks & transforms in Zod v4.6.7:
const SlugSchema = z.string().normalize("NFC").slugify();
const PrefixSchema = z.string().startsWith("pk_live_").length(32);
const NonEmptyText = z.string().nonempty(); // Equivalent to .min(1)

2. Top-Level String Formats (ZodStringFormat)

In Zod v3, string formats were chained onto z.string() (e.g., z.string().email()). In Zod v4.6.7, all string formats are standalone top-level constructors inheriting from ZodStringFormat. While legacy z.string().email() methods remain for compatibility, top-level format functions are cleaner and tree-shake unused regular expressions out of production bundles.

2.1 Emails, URLs & Network Addresses

import { z } from "zod";

// 1. Email validation (customizable regex pattern via z.regexes.*)
const DefaultEmail = z.email();
const Html5Email = z.email({ pattern: z.regexes.html5Email });
const Rfc5322Email = z.email({ pattern: z.regexes.rfc5322Email });
const UnicodeEmail = z.email({ pattern: z.regexes.unicodeEmail });

// 2. URLs with optional protocol and hostname constraints
const AnyUrl = z.url();
const HttpOnlyUrl = z.httpUrl(); // Enforces http:// or https:// and valid domain
const InternalWebhook = z.url({
  protocol: /^https$/,
  hostname: /\.internal\.example\.com$/,
});

// 3. IP addresses, CIDR blocks, MAC addresses, and E.164 phone numbers
const ServerNetSchema = z.object({
  ipv4: z.ipv4(),
  ipv6: z.ipv6(),
  subnetV4: z.cidrv4(), // e.g., "10.0.0.0/24"
  subnetV6: z.cidrv6(),
  macAddress: z.mac({ delimiter: ":" }),
  hostname: z.hostname(),
  oncallPhone: z.e164(), // e.g., "+14155552671"
});

2.2 UUIDs, IDs, JWTs, Hashes & Custom Formats

// UUIDs (any version or pinned to v4/v6/v7)
const AnyUuid = z.uuid();
const UuidV4 = z.uuidv4(); // or z.uuid({ version: "v4" })
const UuidV7 = z.uuidv7(); // Time-ordered UUIDv7

// Modern distributed IDs
const Cuid2Id = z.cuid2();
const UlidId = z.ulid();
const NanoId = z.nanoid();
const KsuidId = z.ksuid();

// Cryptographic tokens, encodings & hashes
const JwtToken = z.jwt({ alg: "HS256" });
const Base64Payload = z.base64();
const Base64UrlToken = z.base64url();
const Sha256Hex = z.hash("sha256", { enc: "hex" }); // md5, sha1, sha256, sha384, sha512

// Custom string format registered with JSON Schema `format`
const SemverFormat = z.stringFormat("semver", /^\d+\.\d+\.\d+$/);

2.3 ISO 8601 Dates, Times & Durations (z.iso.*)

Zod v4 groups all ISO 8601 string schemas under the z.iso namespace:

const EventScheduleSchema = z.object({
  // ISO 8601 datetime: "2026-11-10T10:30:00Z"
  // Supports `offset: true` ("+09:00"), `local: true` (no Z), and `precision: 3`
  startsAt: z.iso.datetime({ offset: true, precision: 3 }),

  // ISO 8601 calendar date: "2026-11-10"
  dateOnly: z.iso.date(),

  // ISO 8601 wall-clock time: "10:30:00"
  timeOnly: z.iso.time({ precision: 0 }),

  // ISO 8601 duration: "P3Y6M4DT12H30M5S"
  window: z.iso.duration(),
});

3. Numbers, Fixed-Width Integers & BigInts

In Zod v4, z.number() rejects NaN, Infinity, and -Infinity by default. Furthermore, dedicated constructors validate safe integers, 32-bit integers, IEEE 754 floats, and 64-bit BigInt ranges:

Constructor Accepted Domain JSON Schema Output
z.number() Finite IEEE 754 number (Number.isFinite) { "type": "number" }
z.int() Safe integers ([Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]) { "type": "integer" }
z.int32() Signed 32-bit integer ([-2147483648, 2147483647]) { "type": "integer", "format": "int32" }
z.uint32() Unsigned 32-bit integer ([0, 4294967295]) { "type": "integer", "format": "uint32" }
z.float32() Single-precision 32-bit float range { "type": "number", "format": "float" }
z.float64() Double-precision 64-bit float range { "type": "number", "format": "double" }
z.bigint() Arbitrary-precision bigint (Requires custom processor or string codec)
z.int64() / z.uint64() Signed / unsigned 64-bit bigint bounds { "type": "integer", "format": "int64" }
const LineItemSchema = z.object({
  quantity: z.int().positive(), // > 0
  unitPriceCents: z.uint32().nonnegative(), // >= 0
  discountRate: z.number().gte(0).lte(1).multipleOf(0.05),
  ledgerBalance: z.int64(), // Validated `bigint` within [-2^63, 2^63 - 1]
});

4. Literals, Enums & Template Literals

4.1 Multi-Value z.literal()

In Zod v4, z.literal() accepts either a single primitive literal or a readonly array of literals:

const Environment = z.literal("production");
const HttpSuccessCode = z.literal([200, 201, 204]); // Inferred as `200 | 201 | 204`

4.2 Unified z.enum() with .extract() and .exclude()

z.enum() accepts both string tuple arrays and native TypeScript enum / as const objects (replacing the deprecated z.nativeEnum()):

const RoleSchema = z.enum(["owner", "admin", "member", "guest"]);
type Role = z.infer<typeof RoleSchema>; // "owner" | "admin" | "member" | "guest"

// Derive subset enums without redeclaring arrays:
const ElevatedRole = RoleSchema.extract(["owner", "admin"]); // "owner" | "admin"
const StandardRole = RoleSchema.exclude(["owner"]); // "admin" | "member" | "guest"

4.3 z.templateLiteral()

Validate structured string patterns and synthesize exact TypeScript template literal types (type CssUnit = ${number}${"px" | "rem" | "%"}``):

const CssDimension = z.templateLiteral([
  z.number(),
  z.enum(["px", "rem", "em", "%", "vh", "vw"]),
]);

type CssDimension = z.infer<typeof CssDimension>;
// => `${number}${"px" | "rem" | "em" | "%" | "vh" | "vw"}`

CssDimension.parse("16px"); // Valid!
CssDimension.parse("1.5rem"); // Valid!

5. Type Coercion (z.coerce.*) vs z.stringbool()

When parsing URL query strings (?page=2&limit=50) or environment variables (process.env), inputs arrive as strings. Zod v4 offers two distinct mechanisms depending on whether you need JavaScript constructor coercion (z.coerce.*) or boolean token parsing (z.stringbool()):

02_primitives_and_formats_2
Comparing Constructor Coercion (z.coerce.*) vs Bidirectional Boolean Token Parsing (z.stringbool())
Source Code
from drawlib.canvas import save, setup
from drawlib.diagrams.flow import Decision, End, FlowDiagram, Process, Start
from drawlib.styles import Colors, Styles

setup(width=168, height=86)

flow = FlowDiagram(
    node_style=Styles.Neutral.patch(text_size=10.5),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.DarkBold.patch(text_size=10.0),
    title="z.coerce.* Constructor Casting vs z.stringbool() Env Var Codec",
    title_style=Styles.DarkBold.patch(text_size=13.0),
    width=156,
    height=70,
    lane_orientation="vertical",
)

flow.add_lane(
    "A. Constructor Coercion (z.coerce.*)",
    width=78,
    style=Styles.Neutral,
    header_style=Styles.PrimaryNeutral.patch(text_size=10.5),
)
flow.add_lane(
    "B. Env Boolean Codec (z.stringbool())",
    width=78,
    style=Styles.Neutral,
    header_style=Styles.SecondaryNeutral.patch(text_size=10.5),
)

c_in = flow.add(
    Start(
        'Query / Form Input\n"42" or "false"',
        width=48,
        height=12,
        style=Styles.Neutral.patch(shape_fill_color=Colors.White, text_size=10.5),
    ),
    xy=(39, 52),
)
c_cast = flow.add(
    Process(
        "JS Constructor Cast\nNumber(x) / Boolean(x)",
        width=50,
        height=12,
        style=Styles.PrimaryNeutral.patch(text_size=10.5),
    ),
    xy=(39, 31),
)
c_out = flow.add(
    End(
        'z.coerce.number("42") -> 42\nBoolean("false") -> true (!)',
        width=54,
        height=12,
        style=Styles.WarningNeutral.patch(text_size=10.0),
    ),
    xy=(39, 10),
)

sb_in = flow.add(
    Start(
        "Environment Variable\nprocess.env.FEATURE_FLAG",
        width=52,
        height=12,
        style=Styles.PrimaryFlat.patch(text_color=Colors.White, text_size=10.5),
    ),
    xy=(117, 52),
)
sb_dec = flow.add(
    Decision(
        "Case-Insensitive\nToken Lookup",
        width=40,
        height=14,
        style=Styles.SecondaryNeutral.patch(text_size=10.0),
    ),
    xy=(117, 31),
)
sb_ok = flow.add(
    End(
        '"true"|"1" -> true\n"false"|"0" -> false',
        width=36,
        height=12,
        style=Styles.SuccessNeutral.patch(text_size=10.0),
    ),
    xy=(97, 10),
)
sb_err = flow.add(
    End(
        "Unknown Token\n-> ZodError",
        width=32,
        height=12,
        style=Styles.DangerNeutral.patch(text_size=10.0),
    ),
    xy=(137, 10),
)

c_in.connect(c_cast, padding=0.6)
c_cast.connect(c_out, padding=0.6)

sb_in.connect(sb_dec, padding=0.6)
sb_dec.connect(sb_ok, start_side="left", end_side="top", padding=0.6)
sb_dec.connect(sb_err, start_side="right", end_side="top", padding=0.6)

flow.draw(xy=(6.0, 4.0))
save()

5.1 Using z.coerce.* for Numbers and Dates

z.coerce.number(), z.coerce.bigint(), z.coerce.date(), and z.coerce.string() invoke the corresponding JavaScript constructor (Number(input), BigInt(input), new Date(input), String(input)) prior to running schema checks:

const PaginationSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  since: z.coerce.date().optional(),
});

PaginationSchema.parse({ page: "3", since: "2026-11-01" });
// => { page: 3, since: Date(2026-11-01T00:00:00.000Z) }

5.2 Why z.stringbool() is Essential for Environment Variables

Because z.coerce.boolean() calls JavaScript's Boolean(val), any non-empty string — including "false" and "0" — coerces to true! For environment variables and CLI flags, always use z.stringbool(), which is a bidirectional ZodCodec<ZodString, ZodBoolean>:

import { z } from "zod";

// Default recognized tokens (case-insensitive):
// truthy: ["true", "1", "yes", "on", "y", "enabled"]
// falsy:  ["false", "0", "no", "off", "n", "disabled"]
const EnvBool = z.stringbool();

EnvBool.parse("true"); // => true
EnvBool.parse("FALSE"); // => false
EnvBool.parse("0"); // => false
EnvBool.encode(true); // => "true" (reverse serialization!)

// Customize accepted tokens and case sensitivity:
const StrictFlag = z.stringbool({
  truthy: ["true", "1"],
  falsy: ["false", "0"],
  case: "sensitive",
});