Read OSS

Architecture and Navigation Guide

Intermediate

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 zshy field in package.json is the build tool configuration. It mirrors the exports map but points to source .ts files, and zshy generates the CJS, ESM, and .d.ts outputs 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 priority
  • localeError — A locale-specific error map (set automatically by classic)
  • jitless — Disables JIT compilation for environments like Cloudflare Workers that restrict eval/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.