TypeScript-first schema declaration and validation library with static type inference
9 Sept 2026 · Newest first
Last checked
Checking sources…
Showing 1 of 1 releases. Full release notes.
Signals highlight changes worth reviewing. They cannot determine whether your application is affected.
⚠️ z.emoji() rejects component-only strings⚠️ Error maps run on the first read of error⚠️ base64 patterns
⚠️ The email pattern dropped its lookaheads
⚠️ Metadata members materialize on first read
⚠️ Numeric enum options no longer include the reverse mappings
⚠️ Chained checks no longer overwrite each other in JSON Schema
The runtime patterns for
z.base64()andz.base64url()are the character sets, with length and padding enforced in code, so a multi-megabyte string can no longer overflow the regex stack through a composed schema. The JSON Schema output still emits the exact block forms, soz.toJSONSchema()is unchanged. (#6534, #6527)
The pattern string is user-visible, and every copy of it changes:
z.regexes.email, which has no capture groups now — neither of the two it used to expose held a usable value;issue.patternon a failedz.email(); and thepatternthatz.toJSONSchema()emits, which no longer carries a lookahead, so validators outside ECMAScript can compile it.
Zod 4.6 is now available.
npm install zod@latest
At a glance:
.validate() — checks input validity without building a result (up to 35x faster than .safeParse().success on a compiled schema)z.instanceof().properties() — validates properties of an instancefromJSONSchema() — enforces six validation keywords it used to ignorez.iban() — electronic-format IBAN plus mod-97 checksumz.withParser() — installs a parser generated elsewhere, for environments without new Functionz.validate() under require)@zod/mini — Zod Mini as a standalone package, versioned in lockstep with zod since 4.5.validate()Standalone boolean validation, in Zod, Zod Mini, and Zod Core. It answers "is this input valid?" without constructing a ZodError, which makes rejection cheap. The return type is a guard on the schema's input type.
z.validate(z.string(), "hi"); // true
z.validate(z.string(), 42); // false
It is a method on Zod Classic schemas too. (#6547)
const Player = z.object({
username: z.string(),
xp: z.number(),
});
if (Player.validate(data)) {
data.username; // narrowed
}
In conjunction with z.compile(), this can be up to 35x faster than .safeParse().success on invalid input.
Time per call on invalid input, compiled with z.compile() — lower is better (benchmark)
Without compilation it is up to 5.9x faster. The saving is the result object: .safeParse() allocates one with an accessor pair on every call, and .validate() allocates nothing.
Time per call on invalid input, plain schemas — lower is better (benchmark)
Both charts measure the failure path. The key feature of .validate() is that it can short-circuit on the first issue it encounters, instead of aggregating a full ZodIssue[] array.
Async refinements are covered by .validateAsync().
z.properties()A new API for validating specific properties of an object. Unlike z.object() it validates in-place, so it plays nice with class instances. (#6536)
const responseLike = z.properties({ status: z.number().min(200).max(299) });
responseLike.parse(new Response("ok", { status: 200 })); // ✅ a real Response
responseLike.parse({ status: 204 }); // ✅ a plain object
A corresponding .properties() method has been added to ZodInstanceOf.
Zod
const okResponse = z.instanceof(Response).properties({
ok: z.literal(true),
status: z.number().min(200).max(299),
});
Zod Mini
const okResponse = z.instanceof(Response).check(...z.properties({
ok: z.literal(true),
status: z.number().check(z.minimum(200), z.maximum(299)),
}));
The input comes back untouched, so the prototype survives and the methods still work. That is the part z.object() cannot do: it would hand back a plain object and the Response would be gone.
const res = await fetch("/api/user");
okResponse.parse(res) === res; // ✅ true
fromJSONSchema()Six additional JSON Schema keywords are now supported in z.fromJSONSchema(). (#6535)
const schema = z.fromJSONSchema({
type: "object",
minProperties: 2, // also maxProperties
});
schema.parse({ a: 1 }); // ❌ too few properties
schema.parse({ a: 1, b: 2 }); // ✅
Both property bounds count the input's own keys. Array uniqueness is structural, so [{ a: 1 }, { a: 1 }] is a duplicate.
z.fromJSONSchema({ type: "array", uniqueItems: true }).parse([{ a: 1 }, { a: 1 }]); // ❌
z.fromJSONSchema({
type: "array",
contains: { type: "number" }, // also minContains and maxContains
minContains: 2,
}).parse(["a", 2]); // ❌ only one number
z.iban()A new string format: an IBAN in electronic format, with a valid ISO 7064 MOD 97-10 checksum. (#6571)
z.iban().parse("DE89370400440532013000"); // ✅
z.iban().parse("DE89370400440532013001"); // ❌ bad checksum
z.withParser()z.compile() builds its parser with new Function, which a strict Content Security Policy blocks. z.withParser() is that installer on its own: it takes a parser generated somewhere else, at build time or by a native compiler, and installs it under the same contract. (#6575)
const Player = z.object({ username: z.string(), xp: z.number() });
// isPlayer is a type guard your build step generated
const Fast = z.withParser(Player, (input) =>
isPlayer(input) ? { username: input.username, xp: input.xp } : z.INVALID
);
The supplied parser owns the whole result, so it has to return what the schema would have returned. This one rebuilds the object rather than handing back its input, because z.object() strips unknown keys. Returning z.INVALID hands the input to the runtime, which stays the only source of ZodErrors.
TypeScript compiles a re-export to a getter, and 252 of the 255 exports on Zod 4.5's CommonJS entrypoint were getters. V8 could not see a constant callee behind one, so it could not inline the call. The 4.6 build emits plain properties and freezes the exports object. On a compiled schema, z.validate() under require is about 3x faster than it was in Zod 4.5. (#6564)
const { z } = require("zod");
const CompiledPlayer = z.compile(Player);
z.validate(CompiledPlayer, data); // ~3x faster than Zod 4.5
Only calls through the namespace were affected. A method call like Player.safeParse(data) never reads the exports object, and the ESM build is unchanged.
A recursive schema held the input and output of its last parse until the next parse replaced it, so one long-lived schema pinned every object it had touched. Zod 4.4 released that input and Zod 4.5 did not, which surfaced as an out-of-memory failure on a repository-wide lint run. The parse state is weak throughout now: one parse of a 29k-node tree retains 2.2 MB where it used to retain 10.1 MB, and recursive parses give up about 6% for it. (#6572)
const Category = z.object({
name: z.string(),
get children() {
return z.array(Category);
},
});
errorBecause safeParse() now builds its error lazily, error maps — global, locale, and per-schema error — run when result.error is first read, not at parse time. Code that swaps z.config() between the parse and the read gets the newer configuration. (#6519)
const result = schema.safeParse(12);
z.config(z.locales.fr());
result.error.issues[0].message; // French in 4.6, English in 4.5
An error map with a side effect never runs if nothing reads the error. Throwing parses are unaffected — .parse() builds and throws its error immediately, never takes the lazy path, and its stack still points at your call site.
z.emoji() rejects component-only stringsUnicode's Emoji_Component property covers the pieces that attach to an emoji, so z.emoji() accepted "123", "#", "*", and a lone zero-width joiner, variation selector, or skin tone modifier. The pattern now requires at least one pictograph, regional indicator, or keycap. (#6532)
z.emoji().parse("😀"); // ✅
z.emoji().parse("1️⃣"); // ✅ the keycap is the anchor
z.emoji().parse("123"); // ❌ was accepted in 4.5
Flags, subdivision flags, skin-tone-modified emoji, and ZWJ sequences are unchanged. Closes #6515.
A numeric TypeScript enum also carries its reverse mapping (0 to "UK") at runtime. The parser already ignored those keys, but .options was read straight off the enum object, so a three-member enum listed six values and three of them failed to parse. (#6542)
enum Country { UK, Germany, France }
z.enum(Country).options; // 4.5: ["UK", "Germany", "France", 0, 1, 2] — 4.6: [0, 1, 2]
The runtime patterns for z.base64() and z.base64url() are the character sets, with length and padding enforced in code, so a multi-megabyte string can no longer overflow the regex stack through a composed schema. The JSON Schema output still emits the exact block forms, so z.toJSONSchema() is unchanged. (#6534, #6527)
Composing z.base64() into a template literal now checks the alphabet but not the length, which is how z.creditCard() already behaves there. The exported z.regexes.base64url is now the length-aware form, so it overflows on a multi-megabyte input the same way z.regexes.base64 does.
z.email() opened with two lookaheads, and the second scanned the whole string before the match began. Both are gone, and the rule they enforced — no empty segment in the local part — is expressed structurally instead, so z.email() accepts and rejects exactly what it did before. Valid addresses validate roughly twice as fast. (#6573)
The pattern string is user-visible, and every copy of it changes: z.regexes.email, which has no capture groups now — neither of the two it used to expose held a usable value; issue.pattern on a failed z.email(); and the pattern that z.toJSONSchema() emits, which no longer carries a lookahead, so validators outside ECMAScript can compile it.
Composing an email into a template literal also stops applying its no-consecutive-dots rule to the rest of the string.
z.templateLiteral([z.email(), "|", z.string()]).parse("a@b.cc|a..b");
// 4.5: ❌ — the lookahead reached past the email segment — 4.6: ✅
Each check used to write its own bounds into the schema as it attached, in chain order, so a format check applied after .min() and .max() replaced the tighter values with its own range. The converter folds the checks as a conjunction now. The order they are chained in no longer changes the output. (#6554, #6553)
z.toJSONSchema(z.number().min(0).max(23).int());
// 4.5: { minimum: -9007199254740991, maximum: 9007199254740991 }
// 4.6: { minimum: 0, maximum: 23 }
Runtime parsing enforced the bounds in every version. Only the emitted schema was wrong. The same fold fixes two more cases: a repeated multipleOf kept the first divisor and dropped the rest, so z.number().multipleOf(2).multipleOf(3) emitted a schema that accepts 4, and z.string().min(8).length(5) emitted minLength: 5, widening a bound the runtime still rejected. Closes #6550.
Eight members on a Zod Classic schema — .format, .minLength, .maxLength, .minValue, .maxValue, .isInt, .minDate and .maxDate — are computed from the checks now instead of being written onto every instance at construction. Each one is a prototype getter that becomes an own property on first read. (#6554)
const s = z.string().min(3).max(9);
Object.keys(s); // 4.5: ["def", "type", "format", "minLength", "maxLength"] — 4.6: ["def", "type"]
s.minLength; // 3 in both
Object.keys(s); // 4.6: ["def", "type", "minLength"]
A key is absent until something reads it, and Object.assign({}, schema) copies only the members that have been read. Deleting one restores the getter, and the next read recomputes it.
The values can move too, because the getters read the same fold the JSON Schema converter does. An order-dependent chain reports the tighter bound now instead of whichever check wrote last.
z.string().min(8).length(5).minLength; // 4.5: 5 — 4.6: 8
Zod 4.6 rolls up 72 commits.
661673ae docs: make the 9thCO logo visible on the light theme by @colinhacks6de10dce docs: reconcile the sponsor listings against every active sponsorship (#6579) by @colinhacks213ee75d feat(compile): add z.withParser for externally generated parsers (#6575) by @colinhacksf9465d4e docs: reconcile the sponsor listings with active sponsorships (#6576) by @colinhacksf7fd5548 perf(v4): drop the lookaheads from the email regex (#6573) by @colinhacks36f17960 fix(v4): stop the memoizer from pinning a finished parse (#6572) by @colinhacks22bed613 feat(v4): add z.iban() string format with mod-97 checksum (#6571) by @colinhacksc5b9bcb3 bench: measure what a runtime island's leaked indent cost the generated source by @colinhacksEach check used to write its own bounds into the schema as it attached, in chain order, so a format check applied after .min() and .max() replaced the tighter values with its own range. The converter folds the checks as a conjunction now. The order they are chained in no longer changes the output. (#6554, #6553)
dcbcf052 fix(compile): unwind the doc indent when a child generator throws (#6570) by @colinhacks277613a6 docs: move the release procedure to the maintainer-local notes by @colinhackseb1c1089 ci: release only on workflow_dispatch behind the npm environment (#6569) by @colinhacks741981ff perf(compile): for-in record walk, cheaper issue finalization, and a generative compile differential (#6567) by @colinhacks804e0f52 perf: seal the CommonJS exports so require("zod") stops reading through a getter (#6564) by @colinhacks6f048367 fix(v4): derive JSON Schema constraints by folding checks in the converter (#6554) by @colinhackse4d67f3e Migrate development and CI to Nub (#6562) by @colinhacks7a002366 fix(v4): don't let format checks overwrite tighter min/max bounds (#6553) by @colinhacks5489a532 test(v4): pin the check-chain case that keeps compiled validate's definite guard (#6551) by @colinhackse7604717 docs: attribute the compiled failure cost to the fallback, not the double pass by @colinhacks764ac59f perf(v4): settle z.validate on the first failure in parse order (#6544) by @colinhacks07917f4c test(v4): pin the lazy safeParse error's stack behavior (#6548) by @colinhacks62e6624b feat(v4): add .validate() and .validateAsync() to Zod Classic (#6547) by @colinhackscafbee47 fix(v4): parse recursive schemas built by a factory (#6530) by @colinhacks4d730882 Release the parsed input once a failing safeParse builds its error (#6543) by @colinhacks90269c60 Keep a numeric TS enum's reverse-mapping keys out of .options (#6542) by @colinhacks18e71c71 Rename the JSON Schema process helper so bundler polyfills cannot collide (#6541) by @colinhacks68aca3dc docs: cover the 4.5 API surface that never made it into the reference by @colinhackseca96871 fix(v4): enforce the six JSON Schema keywords fromJSONSchema silently dropped (#6535) by @colinhacks81ded991 perf: answer z.validate from the compiled fast path on invalid input (#6538) by @colinhacks51caf010 refactor: collapse cachedInternal back into cached (#6540) by @colinhacksabfb3897 feat(v4): make z.properties() a schema, and give z.instanceof() a .properties() method (#6536) by @colinhacks69f2a7ff Collapse toZod's normalizer and move its docs to the API reference (#6539) by @colinhacksbf990216 perf: move util.cached's accessor to a prototype (#6537) by @colinhacksbec73bea perf(v4): build the safeParse error on first read (#6519) by @colinhacks07c43e2a Keep the runtime base64 regexes linear so composed parse paths cannot overflow (#6534) by @colinhacksbc1157e7 docs: use a Response example for z.properties() by @colinhacks2ec972ec refactor: collapse toZod's enum leaf normalizer to a dummy union (#6533) by @colinhacks68a609ac Widen literal inputs in property check types (#6520) by @colinhacks0227e53d docs: bump the star pill's GitHub mark to 20px by @colinhacks84dd3b0f perf: build literal and enum pattern regexes lazily (#6531) by @colinhacksf83ab511 fix(v4): reject component-only strings from z.emoji() (#6532) by @colinhacks74f9a6d3 docs: drop the toZod enum block from basics and pin the page's curation rule in a comment by @colinhacksa2a019a5 Accept enum-typed targets in z.toZod (#6528) by @colinhacks319f47f4 Emit a length-aware base64url pattern in toJSONSchema (#6527) by @colinhacks08ba069e perf(v4): read Luhn digits with charCodeAt instead of string indexing (#6529) by @colinhacks1ec6b7c5 docs: add an RSS feed to the blog at /blog/rss.xml by @colinhacksb801439b bench: add typebox (compiled and dynamic) to the moltar cross-library harness by @colinhacks7ae49d64 docs: drop the circle around the star pill's GitHub mark and center it on the pill's arc by @colinhacks93f3ab32 docs: replace the blog navbar's GitHub icon with a star-count pill by @colinhacksfb2fedfd docs: tighten the memory chart callout, pad the canvas, say "less memory" by @colinhacksff56a551 docs: center the memory chart callout labels and pad them off the number by @colinhacks8cd1250f docs: center the memory chart callout labels by @colinhacks3195ed01 docs: label the memory chart like the compile chart by @colinhacksa6b49390 Mark the compile internals @internal instead of hiding them (#6518) by @colinhacks40b4d0b3 fix(ci): read zod's latest version with npm view when picking the backfill dist-tag by @colinhacks5ff95665 Stop re-exporting the compile internals from zod/v4/core (#6511) by @colinhacksf412178d ci: publish @zod/mini to JSR in lockstep with npm (#6510) by @colinhacksf3e7c72e fix(docs): render the docs 404 page inside the (doc) layout once by @colinhacksf3cb3644 docs: surface the blog on the home page and in the sidebar by @colinhackscd4f9a67 perf(v4): report Standard Schema issues without constructing a ZodError (#6509) by @colinhacks43b9bfc5 docs: drop the bound-methods section from the Zod package page by @colinhacks70eb2c07 docs: drop the traits section and the compilation feature bullet by @colinhacks1c0bce0c docs: bring the 4.5 charts and worked examples into the docs pages by @colinhacksa0898b4b ci: wait hours for npm to serve a publish, not ten minutes (#6502) by @colinhacksc46eeff0 chore: narrow blanket biome-ignore comments (#6504) by @pullfrog[bot]c7ec94d3 ci: check zod and @zod/mini lockstep on npm after every publish (#6507) by @colinhacks81065739 chore(docs): build with Turbopack by @colinhacksabd41adb docs(wiki): move plans and comparisons into a gitignored internal/ (#6506) by @colinhacks2956c4c2 chore(mini): sync @zod/mini to 4.5.4 by @colinhacks8ce9e8d5 feat(mini): publish Zod Mini as the standalone @zod/mini package (#6491) by @colinhacks93186cab docs(wiki): drop the zod-compiler benchmark (#6505) by @colinhacks908c9e17 fix(docs): retry the GitHub stars fetch and log the real status by @colinhacks