1. Installation, Strict Mode & Type Inference
Zod v4.6.7 is designed around a fundamental principle: define your schema once at the boundary of your application, and let TypeScript derive every compile-time type automatically. This chapter covers package installation, why TypeScript's "strict": true compiler flag is required, how to extract z.infer<T>, z.input<T>, and z.output<T> types, and how to choose among Zod's runtime parsing and codec methods.
Source Code
from drawlib.canvas import save, setup
from drawlib.diagrams.flow import End, FlowDiagram, Process, Start
from drawlib.styles import Colors, Styles
setup(width=168, height=78)
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="Zod v4 Single Source of Truth — Static Types + Runtime Execution",
title_style=Styles.DarkBold.patch(text_size=13.0),
width=156,
height=62,
lane_orientation="vertical",
)
flow.add_lane(
"1. Schema Declaration",
width=52,
style=Styles.Neutral,
header_style=Styles.PrimaryNeutral.patch(text_size=10.5),
)
flow.add_lane(
"2. Compile-Time TypeScript",
width=52,
style=Styles.Neutral,
header_style=Styles.PrimaryNeutral.patch(text_size=10.5),
)
flow.add_lane(
"3. Runtime Validation & Codec",
width=52,
style=Styles.Neutral,
header_style=Styles.SecondaryNeutral.patch(text_size=10.5),
)
schema = flow.add(
Start(
"const UserSchema =\n z.object({ ... })",
width=40,
height=15,
style=Styles.PrimaryFlat.patch(text_color=Colors.White, text_size=10.5),
),
xy=(26, 28),
)
t_out = flow.add(
Process(
"z.infer<typeof S>\nz.output<typeof S>",
width=38,
height=14,
style=Styles.PrimaryNeutral.patch(text_size=10.5),
),
xy=(78, 42),
)
t_in = flow.add(
Process(
"z.input<typeof S>\nPre-Transform Input",
width=38,
height=14,
style=Styles.Neutral.patch(shape_fill_color=Colors.White, text_size=10.5),
),
xy=(78, 15),
)
r_parse = flow.add(
End(
"safeParse() / parse()\nValidated Output<T>",
width=38,
height=14,
style=Styles.SuccessNeutral.patch(text_size=10.5),
),
xy=(130, 42),
)
r_codec = flow.add(
End(
"decode() / encode()\nBidirectional Codec",
width=38,
height=14,
style=Styles.SecondaryNeutral.patch(text_size=10.5),
),
xy=(130, 15),
)
schema.connect(t_out, start_side="top", end_side="left", padding=1.0)
schema.connect(t_in, start_side="bottom", end_side="left", padding=1.0)
t_out.connect(r_parse, padding=1.0)
t_in.connect(r_codec, arrow="<->", padding=1.0)
flow.draw(xy=(6.0, 4.0))
save()
1. Installing zod@4.6.7
Zod v4.6.7 has zero third-party runtime dependencies and supports Node.js 18+, Bun, Deno, Cloudflare Workers, and modern browsers via dual ESM (index.js) and CommonJS (index.cjs) entrypoints.
# npm
npm install zod@^4.6.7
# pnpm
pnpm add zod@^4.6.7
# bun
bun add zod@^4.6.7
# yarn
yarn add zod@^4.6.7
Importing Zod Facades
Depending on your bundle size budget and coding style, import from the classic zod entrypoint or the functional zod/mini subpath:
// Classic facade: full method chaining (.min(), .trim(), .refine(), .transform())
import { z } from "zod";
// Tree-shakeable functional facade: minimal prototype footprint
import * as zm from "zod/mini";
// Shared core types & utilities for library authors
import * as core from "zod/v4/core";
2. Mandatory TypeScript Configuration ("strict": true)
Zod v4.6.7 requires TypeScript 5.5 or later with "strict": true (specifically "strictNullChecks": true) enabled in your tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true
}
}
Why is "strict": true Non-Negotiable?
Without "strictNullChecks": true, TypeScript collapses string | undefined and string | null into string. Under non-strict mode:
z.string().optional()cannot distinguish between required and optional object properties inz.infer<typeof Schema>.z.string().nullable()loses nullability tracking at compile time.- Enabling
"exactOptionalPropertyTypes": trueis also supported in Zod v4 via.exactOptional(), which distinguishes a missing key ({}) from an explicitundefinedvalue ({ key: undefined }).
3. Runtime Parsing & Codec Methods Matrix
Every Zod schema exposes twelve runtime execution methods grouped into three pairs (throwing vs. safe, synchronous vs. asynchronous, and untrusted parsing vs. typed codec conversion):
Source Code
from drawlib.canvas import save, setup
from drawlib.smartarts import Table
from drawlib.styles import Colors, Styles
setup(width=168, height=68)
table = Table(
cell_style=Styles.White,
text_style=Styles.Dark.patch(text_size=10.0),
header_cell_style=Styles.PrimaryFlat,
header_text_style=Styles.WhiteBold.patch(text_size=10.5),
border_style=Styles.MutedThin,
)
table.set_style_cell_evenodd(
even_color=Colors.Light,
even_text_style=Styles.Dark.patch(text_size=10.0),
odd_color=Colors.White,
odd_text_style=Styles.Dark.patch(text_size=10.0),
)
table.set_style_cell(
background_color=Colors.Primary1,
text_style=Styles.DarkBold.patch(text_size=10.0),
rows=[2],
)
table.set_style_cell_header(
background_color=Colors.Primary,
text_style=Styles.WhiteBold.patch(text_size=10.5),
)
data = [
["Method Signature", "Input Type", "Sync / Async", "On Validation Failure", "Return Type"],
["schema.parse(data)", "unknown", "Synchronous", "Throws ZodError", "Output<T>"],
["schema.safeParse(data)", "unknown", "Synchronous", "Returns { success: false, error }", "SafeParseResult<Output<T>>"],
["schema.parseAsync(data)", "unknown", "Asynchronous", "Rejects with ZodError", "Promise<Output<T>>"],
["schema.safeParseAsync(data)", "unknown", "Asynchronous", "Resolves { success: false, error }", "Promise<SafeParseResult<T>>"],
["schema.decode(input)", "Input<T>", "Sync (or Async)", "Throws ZodError (safeDecode ok)", "Output<T>"],
["schema.encode(output)", "Output<T>", "Sync (or Async)", "Throws ZodError (safeEncode ok)", "Input<T>"],
]
table.draw_flexible(
xy=(5.0, 62.5),
column_widths=[34.0, 18.0, 22.0, 42.0, 42.0],
row_heights=[8.5, 8.0, 8.0, 8.0, 8.0, 8.0, 8.0],
data=data,
align="top_left",
)
save()
3.1 .parse(data) vs .safeParse(data)
Use .parse(data) when validation failures are exceptional and handled by an outer error boundary or middleware. Use .safeParse(data) when invalid user input is expected and you want a discriminated union result (ZodSafeParseResult<T>):
import { z } from "zod";
const PortSchema = z.int().min(1).max(65535);
// 1. Throwing parse: returns `number` or throws `ZodError`
try {
const port = PortSchema.parse(8080); // 8080
} catch (err) {
if (err instanceof z.ZodError) {
console.error(err.issues);
}
}
// 2. Discriminated union safeParse: never throws
const res = PortSchema.safeParse("8080");
if (!res.success) {
// TypeScript narrows `res` to `ZodSafeParseError<number>`
console.error(z.prettifyError(res.error));
} else {
// TypeScript narrows `res` to `ZodSafeParseSuccess<number>`
const validPort: number = res.data;
}
3.2 Asynchronous Validation (parseAsync & safeParseAsync / spa)
If any refinement (.refine(async (val) => ...)), transformation (.transform(async (val) => ...)), or codec in a schema returns a Promise, calling synchronous .parse() or .safeParse() throws a ZodAsyncError. Always invoke .parseAsync(), .safeParseAsync(), or its built-in shorthand alias .spa():
const UniqueUsername = z
.string()
.min(3)
.refine(
async (username) => {
const taken = await db.users.exists({ username });
return !taken;
},
{ error: "Username is already registered" }
);
// `.spa()` is an exact alias for `.safeParseAsync()`
const check = await UniqueUsername.spa("colinhacks");
3.3 Type Guard Validation (.validate() & .validateAsync())
Zod v4 also provides boolean type-predicate methods (schema.validate(data): data is z.input<typeof schema>) when you only need a TypeScript type guard without constructing a transformed output or ZodError instance:
const ConfigSchema = z.object({ host: z.string(), port: z.int() });
function connectIfValid(raw: unknown) {
if (ConfigSchema.validate(raw)) {
// `raw` is narrowed to `{ host: string; port: number }`
console.log(`Connecting to ${raw.host}:${raw.port}`);
}
}
4. Static Type Extraction (z.infer, z.input, z.output)
Every $ZodType<Output, Input> tracks two static type parameters:
Output: The validated, defaulted, or transformed value returned by.parse()and.decode().Input: The pre-transformation value accepted by.decode()and returned by.encode().
import { z } from "zod";
const SearchQuerySchema = z.object({
q: z.string().trim().min(1),
page: z.int().min(1).default(1),
tags: z
.string()
.transform((s) => s.split(",").filter(Boolean))
.default([]),
});
// 1. z.input<T>: What callers pass IN before defaults & transforms run
type SearchQueryInput = z.input<typeof SearchQuerySchema>;
// => {
// q: string;
// page?: number | undefined;
// tags?: string | undefined;
// }
// 2. z.output<T> (identical to z.infer<T>): What .parse() returns
type SearchQueryOutput = z.output<typeof SearchQuerySchema>;
type SearchQuery = z.infer<typeof SearchQuerySchema>;
// => {
// q: string;
// page: number;
// tags: string[];
// }
Key Takeaways for Type Inference
z.infer<typeof S>andz.output<typeof S>are aliases: Both resolve toS["_zod"]["output"].- When
InputandOutputdiverge: Wrapping a field in.default(...),.prefault(...),.catch(...),.transform(...), orz.codec(...)causesz.input<T>andz.output<T>to differ. Usez.input<typeof S>for API request builder arguments andz.infer<typeof S>for validated domain handlers. - Next Steps: Continue to 2. Primitives, Literals & String Formats to explore Zod v4's built-in scalar types, top-level format validators, template literals, and coercion schemas.