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.
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()):
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",
});
- Next Steps: Proceed to 3. Objects, Records, Tuples & Recursion to combine primitive schemas into structured objects, key-value collections, fixed tuples, and recursive data structures.