What Changed in Zod 4, and How I Migrated Production Schemas

작성자

카테고리:

← 피드로
DEV Community · Ahmed Mahmoud · 2026-07-22 개발(SW)

Ahmed Mahmoud

Headline: Zod 4 is a rewrite of the TypeScript-first schema validation library, released as the stable major in 2025. Four changes hit my code directly: string formats moved to top-level functions (z.email() instead of z.string().email()), the four error options collapsed into one error parameter, error formatting moved to standalone helpers (z.flattenError, z.treeifyError, z.prettifyError), and .strict()/.passthrough() became z.strictObject()/z.looseObject(). The deprecated Zod 3 APIs still work with warnings, so I migrated incrementally.

Key takeaways

  • Zod 4 is the stable major of the TypeScript-first schema validator, released in 2025; it requires TypeScript 5.5 or newer.
  • String formats are now top-level tree-shakeable functions — z.email(), z.uuid(), z.url() — and z.string().email() is deprecated but still works.
  • A single error parameter replaces Zod 3’s message, invalid_type_error, required_error, and errorMap.
  • Error formatting moved to z.flattenError (form fields), z.treeifyError (nested), and z.prettifyError (human-readable string).
  • The zod/mini build exposes the same validators through a functional, tree-shakeable API; z.infer, .parse(), and .safeParse() did not change.

I reach for Zod on almost every project to validate untrusted input at the boundary — request bodies, form data, environment variables, API responses. Zod 4 changed enough of the surface that a mechanical upgrade tripped a handful of files, so I mapped exactly what moved.

What actually changed in Zod 4?

Zod 4 is a ground-up rewrite of the TypeScript-first schema validation library, released as the stable major in 2025. The headline is performance: the Zod team’s release notes report large reductions in TypeScript compiler instantiations and faster runtime parsing, which matters most in large codebases where schema types dominate type-check time. Four API changes touched my code directly — string formats moved to top-level functions, the four error options collapsed into one, error formatting moved to standalone helpers, and .strict()/.passthrough() became z.strictObject()/z.looseObject().

Why did z.string().email() become z.email()?

Zod 4 promotes string formats to standalone top-level functions — z.email(), z.uuid(), z.url(), and the ISO helpers under z.iso — instead of methods chained onto z.string(). The chained form still works but is deprecated and warns. The reason is tree-shaking: each format is its own function, so a bundle that only validates emails no longer ships the logic for every other format.

import * as z from "zod";

const User = z.object({
  id: z.uuid(),
  email: z.email(),
  website: z.url().optional(),
  age: z.number().int().min(18),
});

type User = z.infer<typeof User>;

Enter fullscreen mode Exit fullscreen mode

How do I set custom error messages in Zod 4?

Zod 4 replaces four separate error options with a single error parameter. In Zod 3 you passed message, invalid_type_error, required_error, or a full errorMap. In Zod 4 you pass one error: a string for a fixed message, or a function that receives the issue and returns a message, letting you distinguish a missing value from a wrong type in one place.

// Zod 4: one error param — string or function
z.string({ error: "Name is required" });

z.string({
  error: (issue) =>
    issue.input === undefined ? "Name is required" : "Name must be text",
});

Enter fullscreen mode Exit fullscreen mode

How do I turn a ZodError into form errors?

Zod 4 moves error formatting into three top-level helpers. z.flattenError(error) returns { formErrors, fieldErrors }, which maps onto a form’s field-level messages. z.treeifyError(error) returns a nested object mirroring the schema shape. z.prettifyError(error) returns a human-readable multiline string for logs. The raw error.issues array is unchanged; error.format() and error.flatten() are deprecated in favor of these helpers.

const result = User.safeParse(await request.json());

if (!result.success) {
  const { fieldErrors } = z.flattenError(result.error);
  return Response.json({ errors: fieldErrors }, { status: 400 });
}

const user = result.data; // fully typed as User

Enter fullscreen mode Exit fullscreen mode

When should I use zod/mini instead of the full zod package?

Use zod/mini when bundle size matters — client code, edge functions, a shipped widget — and the full zod package everywhere else. zod/mini exposes the same validators through a functional API: instead of chaining .optional() you wrap with z.optional(), and refinements use .check() rather than .refine(). It is more verbose, but with no method chain, unused code tree-shakes away. Both builds share the same core, so runtime behavior is identical.

import * as z from "zod/mini";

const User = z.object({
  email: z.email(),
  name: z.optional(z.string()),
});

Enter fullscreen mode Exit fullscreen mode

Concern zod (full) zod/mini API style Chained methods (.optional()) Functional wrappers (z.optional()) Bundle size Larger Smaller, tree-shakeable Ergonomics Fluent, readable More verbose Same validators Yes Yes (same core) Best for Servers, general code Edge/client, size-critical bundles

How do I migrate from Zod 3 without breaking everything?

Migrate incrementally, because Zod 4 keeps the deprecated Zod 3 APIs working with warnings rather than removing them. During the transition Zod published the new version under the zod/v4 import path (in [email protected]) so you could adopt it file by file before [email protected]; the legacy API stays reachable at zod/v3. My process: upgrade the package, run the type-checker and tests, then clear deprecation warnings in waves — rename string-format calls, collapse error options into error, and switch .strict()/.passthrough() to the new object functions. z.infer, .parse(), and .safeParse() did not change, so most schemas kept working untouched.

FAQ

Q: Is z.string().email() removed in Zod 4?
A: No. It still works but is deprecated and warns. The recommended form is the top-level z.email(), and both validate identically.

Q: What replaced errorMap and invalid_type_error in Zod 4?
A: A single error parameter — a string for a fixed message or a function that receives the issue for conditional messages.

Q: How do I get field-level errors for a form in Zod 4?
A: Call z.flattenError(error); its fieldErrors maps each schema key to its messages. Use z.treeifyError for nested shapes.

Q: Is zod/mini a separate library?
A: No. It is a build of Zod 4 with the same validators exposed through a functional, tree-shakeable API for smaller bundles.

Q: Does Zod 4 require a specific TypeScript version?
A: Yes. Zod 4 requires TypeScript 5.5 or newer.

Originally published on devya.dev. Also on eng-ahmed.com. Built by Devya Solutions.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다