Zod is a TypeScript-first schema validation library. You use it to validate and transform data, but also to infer types automatically. It's great if you want typesafe APIs, form validation, or runtime safety without writing everything twice.
At UserJot, I use Zod everywhere: backend, frontend, and in between with tRPC. It's the core of our validation logic and one of the reasons why we can ship features so quickly across our app.
And now Zod just got a huge upgrade with version 4. If you haven't checked it out yet, here's everything you should know.
Zod v4 brings faster parsing, smaller bundle sizes, better error messages, real JSON Schema support, and a lot of long-requested features that make daily use easier.
Zod v4 Is Way Faster
Let's start with speed:
- String parsing is ~14x faster
- Array parsing is ~7x faster
- Object parsing is ~6.5x faster
TypeScript compile times have also improved, especially for projects using .extend()
and .omit()
in long chains.
const A = z.object({
a: z.string(),
b: z.string(),
c: z.string(),
d: z.string(),
e: z.string(),
});
const B = A.extend({
f: z.string(),
g: z.string(),
h: z.string(),
});
In Zod 3, this could cause a massive amount of type instantiations. Zod 4 cuts that number down dramatically, which helps when working with complex schemas.
Smaller Bundle, Simpler API: Zod Mini
Zod 4 introduces a smaller variant called Zod Mini, which uses a more functional style. It's meant for projects where bundle size matters — like client-side apps or libraries.
Here's the difference:
Classic:
import { z } from "zod/v4";
z.string().optional();
z.object({ name: z.string() }).extend({ age: z.number() });
Mini:
import { z } from "zod/v4-mini";
z.optional(z.string());
z.extend(z.object({ name: z.string() }), { age: z.number() });
Parsing methods like .parse()
and .safeParse()
still work the same.
Cleaner Error Messages
Zod now has a built-in way to format errors:
try {
schema.parse(data);
} catch (err) {
if (err instanceof z.ZodError) {
console.log(z.prettifyError(err));
}
}
The output is much easier to read:
✖ Unrecognized key: "extraField"
✖ Invalid input: expected string, received number
→ at username
You can also use a single error
option to define custom messages:
z.string().min(5, { error: "Too short." });
z.string({
error: (issue) =>
issue.input === undefined ? "Required" : "Not a string",
});
Recursive Schemas Without Hacks
You can now define recursive and mutually recursive schemas directly — no casting needed.
const Category = z.object({
name: z.string(),
get subcategories() {
return z.array(Category);
},
});
type Category = z.infer<typeof Category>;
It works the way you'd expect and infers the correct types.
JSON Schema Support
You can now convert Zod schemas into JSON Schema directly using:
z.toJSONSchema(schema);
This will include any .describe()
or .meta()
fields automatically:
const mySchema = z.object({
name: z.string().describe("Your name"),
points: z.number().meta({ examples: [10, 20] }),
});
The result:
{
"type": "object",
"properties": {
"name": { "type": "string", "description": "Your name" },
"points": { "type": "number", "examples": [10, 20] }
},
"required": ["name", "points"]
}
Smarter Metadata
You can attach extra metadata to your schemas and manage it through registries.
const myRegistry = z.registry<{ title: string; description: string }>();
myRegistry.add(
z.string().email(),
{ title: "Email", description: "User's email address" }
);
Zod also includes a global registry:
z.string().meta({
title: "Email",
description: "Provide a valid email",
});
New Top-Level Formats
Instead of chaining .email()
to a string, Zod now gives you direct format methods:
z.email();
z.uuidv4();
z.url();
z.iso.date();
They're easier to read and less error-prone.
Real-World Features You'll Actually Use
z.file()
For validating uploaded files:
z.file()
.min(10_000)
.max(1_000_000)
.type("image/png");
z.stringbool()
Parse booleans from strings like "true"
, "false"
, "1"
, "0"
:
z.stringbool().parse("1"); // => true
You can also customize:
z.stringbool({
truthy: ["yes", "enabled"],
falsy: ["no", "disabled"],
});
Refinements Now Chain Properly
You can now refine and still call other methods like .min()
afterward:
z.string()
.refine((val) => val.includes("@"), "Must be email-like")
.min(5);
z.literal([...])
You no longer need to write long unions of literals:
z.literal([200, 201, 202, 204]);
z.templateLiteral()
Define template literal types like:
z.templateLiteral(["hello, ", z.string()]);
// → `hello, ${string}`
z.templateLiteral([z.number(), z.enum(["px", "em", "rem", "%"])]);
// → `${number}px` | `${number}em` | ...
Small but Nice Improvements
-
z.int32()
,z.uint64()
for fixed-width numbers -
.overwrite()
for transforms that don't change the type -
z.discriminatedUnion()
now supports more complex structures (like nested discriminators)
Final Thoughts
At UserJot, we help SaaS teams collect feedback, share their roadmap, and announce product updates — all from one place. It's built for product teams who want to stay close to their users and keep everyone in the loop.
We use Zod throughout the entire codebase, on the backend for validating input, and on the frontend with tRPC to keep everything typesafe across the stack. With Zod v4, things are faster, cleaner, and easier to maintain.
If you're already using Zod, the upgrade is worth trying. And if you're new to it, this version is a great place to start.
Top comments (3)
pretty cool seeing a tool like zod keep pushing things forward - i always start thinking about how much solid validation actually matters in day-to-day building. you ever feel like most projects skip this stuff and pay for it later?
Love how much faster and clearer Zod v4 is, especially the new error formatting - it saves me so much debugging time.
What was the hardest part to update in your app when switching to v4?
I like to use tools and packages which get updates simultaneously.