Zod v4 Migration and New APIs — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Zod v4 Migration and New APIs (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Zod 4 is a ground-up rewrite focused on TypeScript compiler performance, bundle size, and tree-shaking. The core API surface is 95% backward-compatible, but several specific behaviors changed in ways that cause silent bugs if not addressed.
| Metric | Improvement |
|---|---|
| String parsing speed | ~14x faster than Zod 3 |
| Array parsing speed | ~7x faster |
| Object parsing speed | ~6.5x faster |
| TypeScript type instantiations | ~100x fewer (minutes off build times in monorepos) |
| Bundle size (full Zod) | ~13KB gzipped |
| Bundle size (ZodMini) | ~2KB gzipped |
.merge() DeprecatedReplace all .merge() calls with .extend() or shape destructuring:
// Before (Zod 3)
const Combined = SchemaA.merge(SchemaB);
// After (Zod 4) — preferred for performance
const Combined = z.object({ ...SchemaA.shape, ...SchemaB.shape });
// Or
const Combined = SchemaA.extend(SchemaB.shape);error ParameterAll per-schema error configuration is now via a single error parameter:
// Before (Zod 3 — deprecated in Zod 4)
z.string({ invalid_type_error: "Not a string", required_error: "Required" })
z.string().min(3, { message: "Too short" })
// After (Zod 4)
z.string({ error: (issue) => issue.input === undefined ? "Required" : "Must be text" })
z.string().min(3, { error: "Must be at least 3 characters" }).default() Behavior Change — Causes Silent Data LossIn Zod 4, .default() is applied unconditionally when input is undefined, even when wrapped in .optional() or .partial(). This breaks PATCH endpoint patterns:
// Before (Zod 3): partial() with defaults would not apply defaults for missing fields
// After (Zod 4): partial() + default = default ALWAYS applied when field is missing
const Schema = z.object({ status: z.string().default("active") });
Schema.partial().parse({});
// Zod 3: {} (field omitted, stays undefined)
// Zod 4: { status: "active" } (default applied!) ← DATA LOSS RISK on PATCHFix: Create dedicated PATCH schemas without .default() values.
.deepPartial() RemovedUse manual nested .partial() calls instead:
// Before: schema.deepPartial()
// After: manually apply .partial() at each level
const DeepPartial = z.object({
user: z.object({ name: z.string(), address: z.object({ zip: z.string() }) })
.partial()
.extend({ address: z.object({ zip: z.string() }).partial() }),
});z.coerce.* Input Types Now unknownAll coerce schemas now accept unknown input (was the target primitive type in Zod 3):
// Zod 3: z.coerce.number() had input type "number | string | boolean | ..."
// Zod 4: z.coerce.number() has input type "unknown"
// Code that typed raw inputs as the coerced type will have TypeScript errorsString format validators are now top-level functions for tree-shaking:
// Before (Zod 3 — deprecated chains)
z.string().email()
z.string().uuid()
z.string().url()
z.string().ip()
// After (Zod 4 — top-level, tree-shakable)
z.email()
z.uuid()
z.url()
z.ipv4()
z.ipv6()
z.iso.date()
z.iso.datetime()The old chains still work as aliases but are deprecated and will be removed.
// Deprecated instance methods (Zod 3)
error.format()
error.flatten()
// Replacement top-level functions (Zod 4)
z.treeifyError(error)
z.flattenError(error)
z.prettifyError(error)z.nativeEnum() Deprecatedz.enum() now handles native TypeScript enums directly:
enum Direction { Up, Down, Left, Right }
// Before (Zod 3)
z.nativeEnum(Direction)
// After (Zod 4)
z.enum(Direction)z.promise() — removed; validate the resolved value insteadz.record() — now requires explicit key and value schemas: z.record(z.string(), z.number())z.config() — Global ConfigurationReplaces z.setErrorMap(). Set at application startup:
import { z } from "zod";
import { en } from "zod/locales/en";
z.config(en()); // Load locale
z.config({ customError: (issue) => customErrorFn(issue) }); // Custom error handler.meta() — Schema MetadataAttach metadata (id, title, description, examples) to schemas without affecting validation:
const EmailSchema = z.email().meta({
id: "email-field",
title: "Email Address",
description: "User's primary email",
examples: ["[email protected]"],
});
// Generate JSON Schema (for OpenAPI, form generation, etc.)
const jsonSchema = z.toJSONSchema(EmailSchema);Monorepo warning: Schema IDs must be globally unique. Using .meta({ id }) on schemas defined inside dynamically called functions causes "ID already exists in registry" errors. Only apply .meta({ id }) to module-level schema constants.
@zod/mini — Lightweight VariantA functional, ~2KB alternative to full Zod for bundle-sensitive environments:
import { z } from "@zod/mini";
// Uses standalone functions instead of method chaining
import { parse, optional, string, object } from "@zod/mini";Use for: Edge functions, serverless, frontend bundles where Zod adds meaningful size. Use full Zod for: Node.js backends, apps where DX matters more than ~11KB.
z.file() — File ValidationPurpose-built schema for browser File instances (replaces z.instanceof(File) patterns):
const AvatarSchema = z.file()
.min(1) // minimum bytes
.max(5 * 1024 * 1024) // 5MB maximum
.type("image/jpeg", "image/png", "image/webp");z.templateLiteral() — Structured String Patternsconst SemVerSchema = z.templateLiteral(["v", z.number(), ".", z.number(), ".", z.number()]);
// Matches: "v1.2.3", "v0.0.1"
const CssColorSchema = z.templateLiteral(["#", z.string().length(6)]);z.toJSONSchema() — Native JSON Schema Exportconst jsonSchema = z.toJSONSchema(UserSchema);
// Returns valid JSON Schema for use with OpenAPI, form libraries, etc.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.