Zod v4: Faster, Smarter, and More TypeScript-Friendly
Table of Contents
- Introduction To The Tutorial
- Performance Enhancements
- @zod/mini: A Lightweight Alternative
- Metadata in Zod v4
- The Global Registry in Zod v4
- JSON Schema Conversion in Zod v4
- z.interface()in Zod v4
- File Schemas in Zod v4
- Internationalizationin Zod v4
- Error pretty-printingin Zod v4
- Custom Email Regex in Zod v4
- Template Literal Types in Zod v4
- Number Formats in Zod v4
- Stringbool in Zod v4
- Simplified Error Customization in Zod v4
- Upgraded z.discriminatedUnion() in Zod v4
- Multiple Values in z.literal() in Zod v4
- Refinements Inside Schemas in Zod v4
- .overwrite() in Zod v4
- @zod/core
- Conclusion
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
What is Zod v4, and how does it differ from previous versions?
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.
How does the new z.stringbool() feature in Zod v4 simplify handling truthy and falsy string values?
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.
What are the benefits of the z.literal() upgrade in Zod v4, which now supports multiple values?
In Zod v4, z.literal() supports multiple values in a single schema, making it easier to define constant sets and simplifying union definitions.
How does Zod v4 improve error customization compared to Zod v3?
Zod v4 consolidates error customization into a single error function, allowing dynamic error messages based on validation issues, replacing multiple error parameters.
What is the purpose of the z.prettifyError function, and how does it enhance error handling?
z.prettifyError converts ZodError into a human-friendly formatted string, making debugging easier by providing clearer validation feedback.
How does Zod v4 handle internationalization, and what is the role of z.config()?
Zod v4 introduces locale-based error customization through z.config(), allowing developers to configure validation messages in different languages.
What is the significance of the schema registry introduced in Zod v4?
The schema registry enables storing metadata separately from schemas, making metadata management more efficient across large applications.
How does the global registry in Zod v4 help manage reusable schema descriptions?
z.globalRegistry provides a centralized location for storing common metadata, ensuring consistency across applications using JSON Schema-compatible descriptions.
What are the advantages of the improved JSON Schema conversion in Zod v4?
Zod v4 enhances JSON Schema conversion by ensuring better compatibility with validation tools, supporting frontend-backend schema sharing, and enabling automatic API documentation generation.
How does Zod v4 automatically detect the discriminator key in z.discriminatedUnion()?
Zod v4 analyzes schema definitions and identifies the discriminator key automatically. If no shared discriminator is found, an error is thrown at schema initialization.
What are the customization options for truthy and falsy values in z.stringbool()?
Developers can define custom mappings for truthy and falsy values using an options object, specifying which strings should be treated as true or false.
How does Zod v4 streamline error mapping with the new error parameter?
The error function replaces traditional error mapping, allowing direct customization of error messages based on validation issues.
What are the key differences between error handling in Zod v3 and Zod v4?
Zod v3 used separate error parameters like required_error and invalid_type_error, while Zod v4 unifies them under a single error function.
How does Zod v4 enhance the flexibility of defining constants with the upgraded z.literal()?
The ability to define multiple literals in a single schema reduces complexity in defining constant unions and makes schemas more concise.
What are the practical use cases for the new features introduced in Zod v4?
Zod v4's improvements make it ideal for API validation, frontend-backend schema synchronization, error customization, and building modular validation frameworks.
References
Background References
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.