Architecture and Navigation Guide
Prerequisites
- ›Basic TypeScript knowledge
- ›Familiarity with npm packages and module resolution
- ›Understanding of monorepo concepts
Architecture and Navigation Guide
Zod is one of the most widely used TypeScript libraries in the ecosystem, but few developers have read its source code. Version 4 represents a ground-up rewrite with a layered architecture that separates the parsing engine from the user-facing APIs. This article is your map. We'll walk through the monorepo layout, the three-layer architecture, the entry point resolution strategy, and the global configuration system — giving you everything you need to navigate the codebase confidently.
Monorepo Structure and Package Layout
Zod uses a pnpm workspace with all packages living under packages/. The workspace configuration is minimal:
packages:
- packages/*
hoistWorkspacePackages: false
Here's how the packages break down:
| Package | Purpose |
|---|---|
zod |
The main library — contains all source code, schemas, and API layers |
bench |
Performance benchmarks using mitata/tinybench against other validation libraries |
integration |
Integration tests with ecosystem tools (AI SDK, Drizzle, etc.) |
resolution |
Module resolution correctness tests using @arethetypeswrong/cli |
treeshake |
Tree-shaking verification — ensures dead code elimination works |
tsc |
TypeScript compilation performance tests |
docs |
Documentation site |
The root package.json defines workspace-level scripts. Notice the dev command uses tsx --conditions @zod/source, which we'll explore in the entry points section.
graph TD
ROOT["zod monorepo"] --> ZOD["packages/zod<br/>(main library)"]
ROOT --> BENCH["packages/bench<br/>(benchmarks)"]
ROOT --> INT["packages/integration<br/>(ecosystem tests)"]
ROOT --> RES["packages/resolution<br/>(module resolution)"]
ROOT --> TREE["packages/treeshake<br/>(bundle verification)"]
ROOT --> TSC["packages/tsc<br/>(type-checking perf)"]
ROOT --> DOCS["packages/docs<br/>(documentation)"]
ZOD -->|"workspace:*"| BENCH
ZOD -->|"workspace:*"| INT
Tip: If you're exploring the codebase for the first time, ignore everything except
packages/zod/src/v4/. That directory contains the entire v4 implementation.
The Three-Layer Architecture: Core, Classic, Mini
This is the central architectural insight of Zod v4. The codebase is organized into three distinct layers, each building on the one below it:
flowchart TB
subgraph "User-facing APIs"
CLASSIC["v4/classic<br/>Chainable API<br/>z.string().email().min(5)"]
MINI["v4/mini<br/>Functional API<br/>z.string(z.email(), z.minLength(5))"]
end
subgraph "Engine"
CORE["v4/core<br/>Pure parsing engine<br/>~10k lines, zero API opinions"]
end
CLASSIC --> CORE
MINI --> CORE
Core (v4/core/) is the parsing engine. It defines every schema type (prefixed with $, e.g., $ZodString, $ZodObject), the check system, error types, and the parse pipeline. Core has no opinions about how users construct schemas — it only knows how to validate them. The core layer's public surface is exported from packages/zod/src/v4/core/index.ts.
Classic (v4/classic/) wraps core with the familiar chainable API that Zod users know. When you write z.string().email().min(5), you're using classic. It adds methods like .parse(), .optional(), .transform(), and .pipe() to every schema instance. The classic layer also auto-configures English error messages on import — see line 11 of external.ts:
import en from "../locales/en.js";
config(en());
Mini (v4/mini/) wraps core with a minimal functional API designed for bundle-size-sensitive applications. Instead of chaining methods, checks are passed as constructor arguments. Mini does not auto-configure a locale — you bring your own error messages.
The $ prefix convention is important: all core types use it ($ZodString, $ZodType, $constructor). Classic types drop the prefix (ZodString, ZodType). Mini types use a ZodMini prefix (ZodMiniString, ZodMiniType).
Entry Points and the Exports Map
Zod's package.json defines a comprehensive exports map that controls how every import path resolves:
flowchart LR
A["import z from 'zod'"] --> B["src/index.ts"]
B --> C["v4/classic/external.ts"]
D["import 'zod/mini'"] --> E["src/mini/index.ts"]
E --> F["v4/mini/external.ts"]
G["import 'zod/v4/core'"] --> H["v4/core/index.ts"]
I["import 'zod/v3'"] --> J["v3/index.ts"]
The default entry point is remarkably simple — just four lines in src/index.ts:
import * as z from "./v4/classic/external.js";
export * from "./v4/classic/external.js";
export { z };
export default z;
This gives users three ways to import: import z from 'zod', import { z } from 'zod', or import { string, number } from 'zod'.
Each export entry supports three conditions:
| Condition | Purpose | Resolves to |
|---|---|---|
@zod/source |
Development with tsx | Raw .ts source files |
import |
ESM consumers | Compiled .js files |
require |
CJS consumers | Compiled .cjs files |
The @zod/source condition is the clever part. During development, running tsx --conditions @zod/source resolves imports directly to TypeScript source files, eliminating the need for rebuild cycles. The root package.json wires this up with "dev": "tsx --conditions @zod/source".
Tip: The
zshyfield inpackage.jsonis the build tool configuration. It mirrors the exports map but points to source.tsfiles, andzshygenerates the CJS, ESM, and.d.tsoutputs from those sources.
Global Configuration and Locale System Overview
Zod v4 has a lightweight global configuration system defined in core.ts:
export interface $ZodConfig {
customError?: errors.$ZodErrorMap | undefined;
localeError?: errors.$ZodErrorMap | undefined;
jitless?: boolean | undefined;
}
export const globalConfig: $ZodConfig = {};
export function config(newConfig?: Partial<$ZodConfig>): $ZodConfig {
if (newConfig) Object.assign(globalConfig, newConfig);
return globalConfig;
}
Three settings control global behavior:
customError— A user-supplied error map with the highest prioritylocaleError— A locale-specific error map (set automatically by classic)jitless— Disables JIT compilation for environments like Cloudflare Workers that restricteval/new Function()
The locale system ships with 50+ language files under packages/zod/src/v4/locales/. Each locale exports a factory function that returns { localeError: ... }. The English locale (en.ts) defines the Sizable and FormatDictionary patterns that other locales follow — a design we'll explore in depth in Part 6.
Key Files to Start Reading
If you want to understand Zod's internals, here's the recommended reading order:
| Order | File | What you'll learn |
|---|---|---|
| 1 | v4/core/core.ts |
The $constructor function, trait system, global config |
| 2 | v4/core/schemas.ts L185-315 |
$ZodType base — how every schema initializes |
| 3 | v4/core/schemas.ts L326-396 |
$ZodString — the simplest concrete schema |
| 4 | v4/core/parse.ts |
How parse(), safeParse(), and codec functions work |
| 5 | v4/core/checks.ts |
The check system with onattach callbacks |
| 6 | v4/classic/schemas.ts or v4/mini/schemas.ts |
How API layers wrap core |
| 7 | v4/core/api.ts |
Factory functions that connect core to API layers |
Start with core.ts — it's only 138 lines and introduces the most important concept in the codebase: the $constructor function that replaces class inheritance. Once you understand how schemas are constructed, everything else falls into place.
In the next article, we'll dissect that $constructor function line by line and explore the trait-based composition system that makes Zod's architecture possible.