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.

01_getting_started_1
Single Source of Truth — Bridging Compile-Time TypeScript Inference and Runtime Validation
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 in z.infer<typeof Schema>.
  • z.string().nullable() loses nullability tracking at compile time.
  • Enabling "exactOptionalPropertyTypes": true is also supported in Zod v4 via .exactOptional(), which distinguishes a missing key ({}) from an explicit undefined value ({ 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):

01_getting_started_2
Zod v4 Runtime Execution Matrix — Parsing, Safe Parsing, Async Validation & Bidirectional Codecs
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> and z.output<typeof S> are aliases: Both resolve to S["_zod"]["output"].
  • When Input and Output diverge: Wrapping a field in .default(...), .prefault(...), .catch(...), .transform(...), or z.codec(...) causes z.input<T> and z.output<T> to differ. Use z.input<typeof S> for API request builder arguments and z.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.