BasicUtils

Zod v4: Faster, Smarter, and More TypeScript-Friendly

Published: Apr 17, 2025

By: Joseph Horace

#Zod v4
#Zod v4 Tutorial
#Zod v4 features
#What’s new in Zod v4
#TypeScript validation
#Zod TypeScript validation
#Zod performance
#Zod performance improvements
#JSON Schema conversion
#JSON Schema from Zod

Table of Contents

  1. Introduction To The Tutorial
  2. Performance Enhancements
  3. @zod/mini: A Lightweight Alternative
  4. Metadata in Zod v4
  5. The Global Registry in Zod v4
  6. JSON Schema Conversion in Zod v4
  7. z.interface()in Zod v4
  8. File Schemas in Zod v4
  9. Internationalizationin Zod v4
  10. Error pretty-printingin Zod v4
  11. Custom Email Regex in Zod v4
  12. Template Literal Types in Zod v4
  13. Number Formats in Zod v4
  14. Stringbool in Zod v4
  15. Simplified Error Customization in Zod v4
  16. Upgraded z.discriminatedUnion() in Zod v4
  17. Multiple Values in z.literal() in Zod v4
  18. Refinements Inside Schemas in Zod v4
  19. .overwrite() in Zod v4
  20. @zod/core
  21. Conclusion
zod-logo
basicutils.com

Introduction To The Tutorial

In the ever-evolving world of TypeScript validation, Zod has emerged as the strongest and favorite among most TS developers. Its simplicity and power have captivated many. Zod has announced that they are releasing Zod v4! This is a significant leap, given the great changes they have added to the validation framework. It’s faster, smarter, and enhanced.

In this article, we will consider the new features of Zod v4. We will explore how Zod has become more TypeScript-friendly, its performance benchmarks, and break down the breaking changes. You can take it to be a tutorial on Zod v4.

If you have been using Zod—or even considering using it—this is worth a closer look. Let's go!

Performance Enhancements

One of the largest improvements in Zod v4 is efficiency and speed. The engine can now work with deeply nested structures significantly faster. Several benchmarks have been used to test its speed. These include:

  • String parsing benchmark
  • Array parsing benchmark
  • Object parsing benchmark

To test them yourself, follow this procedure:

$ git clone git@github.com:colinhacks/zod.git
$ cd zod
$ git switch v4
$ pnpm install
$ pnpm bench <name>

Replace <name> with the appropriate benchmark name. It can be any of the following: string, array, object-moltar.

Sample Result from Array Parsing Benchmark

$ pnpm bench array
runtime: node v22.13.0 (arm64-darwin)
 
benchmark      time (avg)             (min … max)       p75       p99      p999
------------------------------------------------- -----------------------------
• z.array() parsing
------------------------------------------------- -----------------------------
zod3          162 µs/iter       (141 µs … 753 µs)    152 µs    291 µs    513 µs
zod4       54'282 ns/iter    (47'084 ns … 669 µs) 50'833 ns    185 µs    233 µs
 
summary for z.array() parsing
  zod4
   2.98x faster than zod3

Findings from the Benchmarks

The following observations were made during testing:

  • Reduced validation overhead – Faster schema parsing and execution
  • Smarter type inference – More efficient TypeScript type handling
  • Improved memory usage – Optimized internal operations for better performance

Zod v4 brings major speed improvements, ensuring developers can validate data faster and more efficiently. In the following sections, we will cover the major additions in Zod v4.

@zod/mini: A Lightweight Alternative

@zod/mini is a lightweight Zod version designed for projects that require minimal bundle size while maintaining core validation functionalities.

Why @zod/mini?

  • Optimized for smaller apps
  • Reduced bundle size
  • Maintains essential validation features

How to Use @zod/mini

Here is how to install @zod/mini:

npm install @zod/mini@next

And this is how you would use it:

import * as z from "@zod/mini";
 
z.optional(z.string());
 
z.union([z.string(), z.number()]);
 
z.extend(z.object({ /* ... */ }), { age: z.number() });

Metadata in Zod v4

Zod v4 introduces a new way for handling metadata using a schema registry. Rather than storing the metadata inside the schema itself, Zod stores it in a separate registry, making metadata management easier.

How It Works

import * as z from "zod";
 
const myRegistry = z.registry<{ title: string; description: string }>();

Adding Schemas to Your Registry

const emailSchema = z.string().email();
 
myRegistry.add(emailSchema, { title: "Email address", description: "..." });
myRegistry.get(emailSchema);
// => { title: "Email address", ... }

Using .register() for Convenience

Alternatively, you can also use the .register() method for convenience:

emailSchema.register(myRegistry, { title: "Email address", description: "..." })
// => returns emailSchema

The Global Registry in Zod v4

Zod v4 introduces z.globalRegistry, a centralized registry for storing common JSON Schema-compatible metadata. This feature helps developers keep track of reusable schema descriptions across applications.

Adding Metadata to the Global Registry

z.globalRegistry.add(z.string(), { 
  id: "email_address",
  title: "Email address",
  description: "Provide your email",
  examples: ["naomie@example.com"],
  extraKey: "Additional properties are also allowed"
});

Using .meta() for Convenience

Instead of manually adding schemas to the global registry, you can attach metadata directly using .meta():

z.string().meta({ 
  id: "email_address",
  title: "Email address",
  description: "Provide your email",
  examples: ["naomie@example.com"],
});

JSON Schema Conversion in Zod v4

Zod v4 brings improved JSON Schema conversion, making it easier to integrate with JSON-based validation tools and APIs.

Why JSON Schema Conversion Matters

Helps interop with other validation libraries

Allows schemas to be shared across backend and frontend

Supports auto-generating API documentation

Converting a Zod Schema to JSON Schema

Zod v4 provides a .toJSONSchema() method to convert a schema:

const userSchema = z.object({
  name: z.string(),
  age: z.number().optional(),
});
const jsonSchema = userSchema.toJSONSchema();
console.log(jsonSchema);

Example output:

{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age": { "type": "number" }
  },
  "required": ["name"]
}

z.interface()in Zod v4

Zod v4 introduces z.interface(), a new way to define object schemas with better optional property handling and true recursive types.

Exact(er) Optional Properties

In TypeScript, a property can be optional in two ways:

  • Key optional: The key may be omitted.

Value optional: The key is required, but the value can be undefined.

Zod 3 couldn’t fully differentiate between these, so Zod 4 adds z.interface(), allowing precise control:

const ValueOptional = z.interface({ name: z.string().optional()}); 
// { name: string | undefined }
const KeyOptional = z.interface({ "name?": z.string() }); 
// { name?: string }

Here, the ? suffix defines key optionality, making schema behavior more predictable.

True Recursive Types

Zod v4 also simplifies defining recursive structures. Previously, developers had to use z.lazy() and extra TypeScript casting. Now, z.interface() allows recursive properties naturally using getters:

const Category = z.interface({
  name: z.string(),
  get subcategories() {
    return z.array(Category);
  }
});

This eliminates the need for workarounds, making recursive schemas cleaner.

File Schemas in Zod v4

Zod v4 introduces file validation, allowing developers to define schemas for File instances with constraints on size and type.

Validating File Instances

You can create a file schema using z.file():

const fileSchema = z.file();
 
fileSchema.min(10_000); // minimum .size (bytes)
fileSchema.max(1_000_000); // maximum .size (bytes)

fileSchema.type("image/png"); // MIME type

International - ization in Zod v4

Zod v4 introduces a new way of working with multiple languages:

import * as z from "zod";
 
// configure English locale (default)
z.config(z.locales.en());

Error pretty-printingin Zod v4

Zod now internally provides an API similar to zod-validation-error. The z.prettifyError function converts ZodError into a friendly, formatted string. Simply pass your error to the function, and Zod handles the rest.

z.prettifyError(myError);

Custom Email Regex in Zod v4

Zod v4 enhances email validation by allowing developers to define custom regular expressions for z.email(). Since there is no universally perfect email regex, different applications may require varying levels of strictness.

Built-in Email Regex Options

Zod provides several predefined regex patterns for common use cases:

// Zod's default email validation (Gmail-style rules)
z.email(); // Uses z.regexes.email
// Browser-standard email validation (HTML5)
z.email({ pattern: z.regexes.html5Email });
// Classic email regex following RFC 5322
z.email({ pattern: z.regexes.rfc5322Email });
// Loose validation supporting Unicode characters
z.email({ pattern: z.regexes.unicodeEmail });

Template Literal Types in Zod v4

This allows developers to define complex string formats in a structured way.

Defining Template Literal Types

You can create dynamic string patterns using z.templateLiteral():

const hello = z.templateLiteral(["hello, ", z.string()]);
// `hello, ${string}`
 
const cssUnits = z.enum(["px", "em", "rem", "%"]);
const css = z.templateLiteral([z.number(), cssUnits]);
// `${number}px` | `${number}em` | `${number}rem` | `${number}%`
 
const email = z.templateLiteral([
  z.string().min(1),
  "@",
  z.string().max(64),
]);
// `${string}@${string}` (the min/max refinements are enforced!)

Number Formats in Zod v4

Zod v4 introduces new numeric formats for handling fixed-width integer and float types with built-in constraints.

.Standard Number Formats

  • These formats ensure proper min/max constraints for standard numbers:
z.int();      // [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]
z.float32();  // [-3.4028234663852886e38, 3.4028234663852886e38]
z.float64();  // [-1.7976931348623157e308, 1.7976931348623157e308]
z.int32();    // [-2147483648, 2147483647]
z.uint32();   // [0, 4294967295]

Stringbool in Zod v4

Zod v4 introduces z.stringbool(), which maps common truthy and falsy strings to actual true or false values.

How to Use It

const strbool = z.stringbool();
strbool.parse("true");      // => true
strbool.parse("1");         // => true
strbool.parse("yes");       // => true
strbool.parse("on");        // => true
strbool.parse("enable");    // => true
strbool.parse("false");     // => false
strbool.parse("0");         // => false
strbool.parse("no");        // => false
strbool.parse("off");       // => false
strbool.parse("disabled");  // => false

Customizing Truthy and Falsy Values

You can also customize the truthy and falsey values like below:

z.stringbool({
  truthy: ["yes", "true"],
  falsy: ["no", "false"]
});

Simplified Error Customization in Zod v4

Zod v4 introduces a major change in error customization, replacing multiple error parameters with a single error function.

Comparing Zod 3 and Zod 4 Error Handling

// Zod 3
- z.string({ 
-   required_error: "This field is required" 
-   invalid_type_error: "Not a string", 
- });
 
// Zod 4 
+ z.string({ error: (issue) => issue.input === undefined ? 
+  "This field is required" :
+  "Not a string" 
+ });

And here is how we would handle error maps in In zod 3 and 4 for comparison:

// Zod 3 
- z.string({
-   errorMap: (issue, ctx) => {
-     if (issue.code === "too_small") {
-       return { message: `Value must be >${issue.minimum}` };
-     }
-     return { message: ctx.defaultError };
-   },
- });
 
// Zod 4
+ z.string({
+   error: (issue) => {
+     if (issue.code === "too_small") {
+       return `Value must be >${issue.minimum}`
+     }
+   },
+ });

Upgraded z. discriminated - Union() in Zod v4

Zod v4 makes discriminated unions smarter and easier to use.

Automatic Discriminator Key Detection

Previously, you had to explicitly specify the discriminator key. Now, Zod automatically identifies it. If no shared key is found, an error is thrown at schema initialization.

// In Zod 4 (automatic key detection)
const myUnion = z.discriminatedUnion([
  z.object({ type: z.literal("a"), a: z.string() }),
  z.object({ type: z.literal("b"), b: z.number() }),
]);
// In Zod 3 (manual key specification)
const myUnion = z.discriminatedUnion("type", [
  z.object({ type: z.literal("a"), a: z.string() }),
  z.object({ type: z.literal("b"), b: z.number() }),
]);

Composing Discriminated Unions

Zod now allows nested discriminated unions, meaning one can be used inside another. Zod chooses the best strategy automatically.

const BaseError = z.object({ status: z.literal("failed"), message: z.string() });
const MyErrors = z.discriminatedUnion([
  BaseError.extend({ code: z.literal(400) }),
  BaseError.extend({ code: z.literal(401) }),
  BaseError.extend({ code: z.literal(500) }),
]);
const MyResult = z.discriminatedUnion([
  z.interface({ status: z.literal("success"), data: z.string() }),
  MyErrors
]);

Multiple Values in z.literal() in Zod v4

Zod v4's z.literal() now supports multiple values, making it more flexible compared to previous versions.

The code that follows demonstrates how to deal with multiple values in z.literal in both versions 3 and 4.

const httpCodes = z.literal([ 200, 201, 202, 204, 206, 207, 208, 226 ]);
 
// previously in Zod 3:
const httpCodes = z.union([
  z.literal(200),
  z.literal(201),
  z.literal(202),
  z.literal(204),
  z.literal(206),
  z.literal(207),
  z.literal(208),
  z.literal(226)
]);

Refinements Inside Schemas in Zod v4

Previously in Zod 3, refinements were stored in a separate ZodEffects class, which made it impossible to mix .refine() with other schema methods like .min(). This led to frustrating limitations when chaining validations.

Old Behavior in Zod 3 (Issue with Refinements)

z.string()
  .refine(val => val.includes("@"))
  .min(5); 
// ❌ Property 'min' does not exist on type ZodEffects<ZodString, string, string>

New Behavior in Zod 4 (Refinements Inside Schemas)

Now, refinements are directly part of the schema, allowing seamless chaining with other methods:

z.string()
  .refine(val => val.includes("@"))
  .min(5); 
// ✅ Works as expected!

.overwrite() in Zod v4

Zod v4 introduces .overwrite(), a new method for defining transformations without altering the inferred type.

Why .overwrite()?

Previously, .transform() allowed arbitrary changes to a value, but it made type introspection impossible at runtime. This meant schemas couldn’t be cleanly converted to JSON Schema.

Example of .transform() losing type introspection:

const Squared = z.number().transform(val => val ** 2);
// => ZodPipe<ZodNumber, ZodTransform> (type introspection lost)

How .overwrite() Works

Unlike .transform(), .overwrite() keeps the original type intact:

z.number().overwrite(val => val ** 2).max(100);
// => ZodNumber (type remains intact)

@zod/core

Zod v4 introduces @zod/core, a package that contains the core validation functionality shared between Zod and @zod/mini. While not directly relevant for most Zod users, this addition significantly expands Zod’s capabilities.

Why @zod/core Exists

  • The creation of @zod/mini (a lightweight version of Zod) required a shared foundational package.
  • @zod/core makes Zod more modular, allowing developers to build custom schema libraries on top of it.
  • Zod evolves from being just a validation library to a flexible validation engine that can power other frameworks.

Who Should Use @zod/core?

If you’re building a schema validation library, you can refer to the implementations of Zod and @zod/mini to see how they use @zod/core as a foundation. Developers are encouraged to discuss ideas and seek guidance on GitHub or platforms like X/Bluesky.

Conclusion

Zod v4 is a massive upgrade that brings better performance, smarter validation, and stronger TypeScript integration. From precise optional properties and recursive types to enhanced JSON Schema conversion and file validation, this version makes schema definition more powerful and flexible than ever.

With new features like z.interface(), @zod/mini, and an extensible core, Zod isn’t just a validation library—it’s evolving into a foundational tool for TypeScript projects. Whether you're working on API validation, form handling, or complex data structures, Zod v4 empowers developers to write safer, more predictable code.

If you're already using Zod—or thinking about it—this update is worth exploring. The improvements make validation faster, easier, and more intuitive, solidifying Zod as the go-to choice for TypeScript validation.

Frequently Asked Questions

Zod v4 introduces significant enhancements, including better TypeScript integration, improved error handling, automatic discriminator detection in unions, and new features like z.interface() for precise optional property control.

z.stringbool() converts common truthy and falsy strings into actual boolean values. It maps 'true', 'yes', 'on', 'enable' to true, while 'false', 'no', 'off', 'disabled' convert to false.

In Zod v4, z.literal() supports multiple values in a single schema, making it easier to define constant sets and simplifying union definitions.

Zod v4 consolidates error customization into a single error function, allowing dynamic error messages based on validation issues, replacing multiple error parameters.

z.prettifyError converts ZodError into a human-friendly formatted string, making debugging easier by providing clearer validation feedback.

Zod v4 introduces locale-based error customization through z.config(), allowing developers to configure validation messages in different languages.

The schema registry enables storing metadata separately from schemas, making metadata management more efficient across large applications.

z.globalRegistry provides a centralized location for storing common metadata, ensuring consistency across applications using JSON Schema-compatible descriptions.

Zod v4 enhances JSON Schema conversion by ensuring better compatibility with validation tools, supporting frontend-backend schema sharing, and enabling automatic API documentation generation.

Zod v4 analyzes schema definitions and identifies the discriminator key automatically. If no shared discriminator is found, an error is thrown at schema initialization.

Developers can define custom mappings for truthy and falsy values using an options object, specifying which strings should be treated as true or false.

The error function replaces traditional error mapping, allowing direct customization of error messages based on validation issues.

Zod v3 used separate error parameters like required_error and invalid_type_error, while Zod v4 unifies them under a single error function.

The ability to define multiple literals in a single schema reduces complexity in defining constant unions and makes schemas more concise.

Zod v4's improvements make it ideal for API validation, frontend-backend schema synchronization, error customization, and building modular validation frameworks.

References

Background References

  1. (April 15, 2025). Introducing Zod 4 beta. *Zod*. Retrieved April 15, 2025 from https://v4.zod.dev/v4
  2. (April 14, 2025). Migration guide. *Zod*. Retrieved April 14, 2025 from https://v4.zod.dev/v4/changelog

About the Author

Joseph Horace

Horace is a dedicated software developer with a deep passion for technology and problem-solving. With years of experience in developing robust and scalable applications, Horace specializes in building user-friendly solutions using cutting-edge technologies. His expertise spans across multiple areas of software development, with a focus on delivering high-quality code and seamless user experiences. Horace believes in continuous learning and enjoys sharing insights with the community through contributions and collaborations. When not coding, he enjoys exploring new technologies and staying updated on industry trends.