
Zod 4 is Here! Everything You Need To Know
In this blog, let's explore what's new with Zod 4, how it is better than Zod 3, how to upgrade to Zod 4, and everything in between.

Kaushal Joshi
May 23, 2025 • 11 min read
If you've ever worked with forms on the web or handled complex API requests and responses, you've probably come across Zod. It’s a TypeScript-first schema declaration and validation library, perfect for validating dynamic data like user inputs, API request parameters, environment variables, and more. The beauty of Zod is that you define your schema once, and TypeScript generates the types for you, ensuring your data is always in sync with your code. It’s a go-to tool for building robust, type-safe forms and APIs.
It’s hard to believe, but it’s been almost four years since Zod’s last major release! During this time, over 24 minor versions have shipped, solidifying Zod as one of the most trusted and widely adopted schema validation libraries for TypeScript developers. Recently, the Zod team announced Zod 4, a significant update that introduces an entirely new internal architecture, addressing long-standing design challenges and unlocking powerful new features.
In this blog, let's explore what's new with Zod 4, how it is better than Zod 3, how to upgrade to Zod 4, and everything in between.
What Is Zod Anyway?
Zod is a TypeScript-first schema declaration and validation library. In simple terms, it helps you define the shape of your data and then ensures that any data passing through your application matches that shape. This makes it perfect for tasks like validating API inputs, sanitizing user data, and enforcing strict type safety without all the boilerplate code.
Features of Zod:
Type Inference: Define your schema, and Zod generates TypeScript types for you, reducing the risk of mismatched data.
Expressive API: Its intuitive, chainable API lets you express complex data structures in a clean, readable way.
First-Class TypeScript Support: Zod is built from the ground up with TypeScript in mind, making it a natural fit for modern TypeScript projects.
Blazing Fast: Designed to be fast and lightweight, minimizing the impact on your application's performance.
Validation and Transformation: Go beyond basic type checking with built-in methods for refining and transforming data.
Defining your first Zod schema:
import { z } from "zod";
const UserSchema = z.object({
name: z.string(),
age: z.number().int().positive(),
email: z.string().email(),
isAdmin: z.boolean().optional(),
});
type User = z.infer<typeof UserSchema>;
// Example validation
try {
const validUser = UserSchema.parse({
name: "Alice",
age: 30,
email: "alice@example.com",
isAdmin: false,
});
console.log(validUser);
} catch (err) {
console.error(err.errors);
}
In this example, UserSchema strictly defines what a valid user object should look like, ensuring only correctly typed data enters your app. It even auto-generates the User TypeScript type, keeping your data model consistent and type-safe.
What's New With Zod 4?
Let’s look at the improvements and changes that are introduced in the latest version of Zod.
Please note that Zod 4 is still in beta. That means active development is still ongoing as of the time of writing. I'll include the official link at the end for the latest updates and migration guides.
Zod 4 is blazingly fast
20x reduction in
tsx
instantiations7x faster object parsing
3x faster array parsing
2.6x faster string parsing
2x reduction in core bundle size
TypeScript performance is faster now
The core bundle size is around 57% smaller in Zod 4. Well, the Zod team is not satisfied with it. They think they can do a lot better, and I absolutely trust them! :)
@zod/mini
Zod APIs are difficult to tree-shake. Even methods as simple as z.boolean()
pulls in the implementation of a bunch of other methods, which are not used all the time, like .optional()
, .array()
, etc. Hence, @zod/mini
is released along with Zod 4.
@zod/mini
provides functional, tree-shakable APIs that correspond one-to-one with Zod methods. It uses wrapper functions instead of places where Zod would have used methods.
Consider writing a schema for an optional string, a string or a number, or a complex object.
import * as z from "zod";
z.string().optional();
z.string().or(z.number());
z.object({ /* ... */ }).extend({ age: z.number() });
Here’s the @zod/mini
way of doing it:
import * as z from "@zod/mini";
z.optional(z.string());
z.union([z.string(), z.number()]);
z.extend(z.object({ /* ... */ }), { age: z.number() });
It also provides some top-level refinements which correspond to various Zod methods we used quite often: z.positive()
, z.negative()
, z.lowercase()
, z.uppercase()
, just to name a few of them.
@zod/mini
is smaller…
When built with rollup
, the gzipped bundle size is 1.85kb. This is almost 85% (basically, 6.6x!) smaller than the core bundle size when compared with zod@3
.
In frontend projects where every kilobyte counts, having a library like @zod/mini
would help minimize the bundle size of the application.
Extensible foundation for Zod: @zod/core
As there are two similar packages, there was a need to create a core functionality shared between zod
and @zod/mini
. Hence, the @zod/core
package was created to store the core functionality shared between the other two libraries.
This package exports the core classes and utilities that are consumed by zod
and @zod/mini
. It is not intended to be used directly. Instead, it's designed to be extended by other packages.
Add Strongly Typed Metadata To Schemas
Zod 4 introduces a new system for adding strongly typed metadata to your schemas. The schema is stored in a separate 'schema registry' that associates schemas with typed metadata.
You can create a new registry with z.registry()
.
import * as z from "zod";
const registry = z.registry({
title: string;
author: string;
})
To add a schema to the registry:
const schema = z.string();
registry.add(schema, {
title: "What's new with Zod 4?",
author: "Kaushal Joshi"
})
And to get registry info, you can use .get()
method:
registry.get(schema);
// Console output:
// { title: "What's new with Zod 4?", author: "Kaushal Joshi" }
New way to define objects: z.interface()
If you’re scratching your head at this point, you’re not alone. Isn't there already a way to define objects? What's the need for this? Reading this section of the blog multiple times
In TypeScript, a property can be optional in two ways. It can either have optional keys or optional values. Confused already? Let me explain.
type KeyOptional = { value?: string, required: number };
type ValueOptional = { value: string | undefined, required: number };
in KeyOptional
, the value
key can be omitted from the object. TypeScript won't yell at you if your object just contains one key, required
. However, in ValueOptional
, the value
key must be present. If it doesn't have any value, it can be set to undefined
.
Zod 3 doesn't have a mechanism that represents ValueOptional
. Instead, it automatically adds a question mark to any key that accepts underfned
, making it like KeyOptional
.
z.object({ name: z.string().optional() });
// { name?: string | undefined }
z.object({ name: z.union([z.string(), z.undefined()]) });
// { name?: string | undefined }
To properly represent "key optionality", Zod needed an object-level API for marking keys as optional, instead of trying to guess based on the value schema. This is why Zod 4 introduces a new API for defining object types: z.interface().
const ValueOptional = z.interface({ name: z.string().optional()});
// { name: string | undefined }
const KeyOptional = z.interface({ "name?": z.string() });
// { name?: string }
Note: The
z.object()
API is not deprecated; feel free to continue using it if you prefer it! For the sake of backwards compatibility,z.interface()
was added as an opt-in API.
True recursive types with z.interface()
Previously in Zod 3, if you wanted to implement Zod recursive types, this is how you’d have implemented it:
import * as z from "zod"; // zod@3
interface Category {
name: string;
subcategories: Category[];
};
const Category: z.ZodType<Category> = z.object({
name: z.string(),
subcategories: z.lazy(() => Category.array()),
});
In short, to define a cyclical object type, you must define a redundant interface, then use z.lazy()
to avoid reference errors. And finally, cast your schema to z.ZodType
. A lot of work! In Zod 4, it is simplified and made straightforward:
import * as z from "zod"; // zod@4
const Category = z.interface({
name: z.string(),
get subcategories() {
return z.array(Category)
}
});
Just use a getter to define a cyclical property, and done. The resulting instance has all the object methods you expect, like Category.pick({ subcategories: true })
.
Validate File Instances Using z.file()
Yes, you read it right! This is a great feature that I am personally excited about! Validating file types is a very useful feature that I will use every time I need to handle files.
You can check the minimum and maximum sizes in bytes, and also validate the MIME types of the file. Very tiny setup features, yet most useful.
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
Translate Error Messages into Different Languages with locales
API
With the new locales API, you can translate error messages into different languages
// configure English locale (default)
z.config(z.locales.en());
Currently, only the English language is available. But soon, more languages will be supported in the future.
Pretty-print Errors
Previously, we needed to use the zod-validation-error library to get user-readable error messages in Zod. Although a simple trick, it was still an extra step and increased your bundle size.
Zod 4 provides a top-level function that converts a ZodError
to user user-readable formatted string. As the team is still working on improving this, the format of the error messages may change in the future, hence, I am not adding the example here. You can check it here.
Simplified error customization
There’s one more improvement in error handling. In Zod 4, there’s now a single error
parameter to customize errors, replacing APIs like message
, invalid_type_error
, required_error
, errorMap
, etc.
z.string().min(5, { error: "Too short." });
z.string({ error: (issue) => issue.input === undefined ?
"This field is required" :
"Not a string"
});
z.string({
error: (issue) => {
if (issue.code === "too_small") {
return `Value must be >${issue.minimum}`
}
},
});
Top-level String Formats
All string formats like email, date, datetime, etc, were used along with z.string()
in Zod 3. In Zod 4, all such string formats are promoted to top-level functions on the z
module.
Old methods like z.string().email()
are still available to use, but have been deprecated. They will be removed in the next major version.
You can find the list of all top-level string formats here.
Customize email regex
The z.email()
API that we just saw supports custom regex expressions. You can decide how strict you want your regex to be. Zod by default exports some common ones:
// Zod's default email regex (Gmail rules)
// see colinhacks.com/essays/reasonable-email-regex
z.email(); // z.regexes.email
// the regex used by browsers to validate input[type=email] fields
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email
z.email({ pattern: z.regexes.html5Email });
// the classic emailregex.com regex (RFC 5322)
z.email({ pattern: z.regexes.rfc5322Email });
// a loose regex that allows Unicode (good for intl emails)
z.email({ pattern: z.regexes.unicodeEmail });
Template Literal Types
Template literal types are one of the most important features of TypeScript. However, it wasn’t implemented in previous versions of Zod. However, Zod 4 implements z.templateLiteral()
to define template literals.
Every Zod schema type that can be strigified stores an internal regex: strings, string formats like z.email()
, numbers, boolean, bigint, enums, literals, undefined/optional, null/nullable, and other template literals. The z.templateLiteral
constructor concatenates these into a super-regex, so things like string formats (z.email()
) are properly enforced (but custom refinements are not!).
In short, you can convert Zod types directly into TypeScript template literal types. Let’s see some examples:
const hello = z.templateLiteral(["hello, ", z.string()]);
This results in a string type that starts with hello,
, and is followed by a string.
hello, ${string}
Here’s another example that simplifies it:
const cssUnits = z.enum(["px", "em", "rem", "%"]);
This creates a union of four different strings, and then the union is passed in z.templateLIteral
and prefixed by a number. This would result in a union where you can represent CSS units.
${number}px` | `${number}em` | `${number}rem` | `${number}%
This is important because it brings Zod much closer to the expressive power of TypeScript itself.
Number formats
Zod 4 provides a way to represent fixed-width integers with float types. These return a ZodNumber
instance where minimum and maximum constraints are already added.
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]
z.int64(); // [-9223372036854775808n, 9223372036854775807n]
z.uint64(); // [0n, 18446744073709551615n]
Wrapping Up
Zod 4 is undoubtedly a huge step up from Zod 3, making it the unanimous choice for schema validation in TypeScript apps. This version will remain in beta for a few more weeks until the version is officially released for the general public.
You can refer to the announcement blog to know more about Zod 4.
If you want to upgrade to Zod 4 Beta and test out its new features, you can install it with a simple command:
pnpm upgrade zod@next
Give it a try and let me know what you build! I am most active on Peerlist and Twitter if you want to say hi!
If you are looking for a new job or want to share a cool project you built, consider Peerlist! It is a professional network for builders to show & tell!
Until then, happy parsing!