Zod's z.compile() only takes the fast path when your input is valid
TypeScript-first schema validation with static type inference
At a glance
- What is it?
- Zod is a TypeScript-first validation library with no external dependencies and a 2 kB gzipped core, and its v4 line adds ahead-of-time compilation through z.compile(). The interesting parts are the edges: compilation calls new Function, async refinements silently move you to parseAsync, and an invalid input falls back to the regular parser, which means your refinements can run twice.
- Who is it for?
- Choose Zod when you want one schema to drive both validation and the static type, when you can accept new Function as a runtime requirement or can set jitless, and when your hot paths parse structured objects large enough to pay for compilation. Do not choose it for a single z.string() check, where the README's own benchmark says compilation gains nothing, and think twice before compiling a schema whose refinements have side effects, since they run twice on invalid input.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Valid inputs take the compiled path, invalid ones fall back
z.compile() returns a schema clone with an ahead-of-time compiled fast path, and the fallback is the part worth understanding:
const CompiledPlayer = z.compile(Player);
CompiledPlayer.parse({ username: "billie", xp: 100 });Valid input goes through generated code. Invalid input is handed to the regular parser instead, and the stated reason is that error reporting stays identical, so a compiled schema does not give you a cheaper error path. The numbers the README gives come from a 55-schema benchmark with a median speedup of 2.4x, and the spread is the useful part rather than the median: a large array of objects and a 20-key object are both around 9x, a nested object around 4.5x, and a bare z.string() gains nothing. The explanation given is that compilation removes per-node dispatch and allocation, and a single typeof has neither. So the decision to compile belongs on the schemas that do real structural work, not on the whole codebase by reflex. Because the API is immutable, methods return a new instance, which is also why a schema derived from a compiled one is no longer compiled.
Compilation calls new Function, so a strict CSP disables it
The runtime requirement is stated in plain text: compilation uses new Function. That is a problem anywhere a Content Security Policy forbids dynamic code evaluation, and the library's answer is a configuration flag rather than a fallback. Global mode is automatically disabled when jitless is set:
import "zod/compile"; // place before modules that define schemasGlobal mode is opt-in through a side-effect import, and the placement matters, because it has to be evaluated before the modules that construct your schemas. Calling z.compile() directly is treated as an explicit opt-in instead, so a codebase can be mixed. Three consequences for a browser application. First, under a strict CSP you must set jitless and lose global compilation, leaving only the explicit per-schema calls, which still generate code. Second, any server-side rendering path that evaluates the same modules has to agree with the browser about the flag, or the two environments take different paths. Third, an environment test that checks behaviour rather than performance will not notice the difference, since compiled and uncompiled schemas return the same values. The strict form of the flag matters for debugging: passing { strict: true } to z.compile() turns the silent cases into ZodCompileAsyncError and ZodCompileUnsupportedError.
An async refine moves every call site to parseAsync
Schemas that use async refinements or transforms cannot use the synchronous entry points, and the README marks this as a note rather than a warning:
const schema = z.string().refine(async (val) => val.length <= 8);
await schema.parseAsync("hello");
// => "hello"The same split applies to error handling, where .safeParse() has an async counterpart named .safeParseAsync(). The practical hazard is not the schema, it is the migration. A schema that starts as a synchronous z.string() and later grows one async check, a database uniqueness lookup for instance, turns every .parse() call site into a runtime failure rather than a type error, because the call signature still looks valid. Two habits reduce the damage: keep async checks in a separate schema from the synchronous shape, and treat the choice of parse method as part of the schema's contract rather than a detail of the call site. It also interacts with compilation, since a schema with an async refinement is one of the constructs z.compile() cannot handle.
parse hands back a deep clone, not your object
On success, .parse() returns a strongly typed deep clone of the input rather than the same reference. That single design choice has effects in both directions. It means a value that passed validation cannot be mutated afterwards by code holding the original object, which removes a class of aliasing bug where a request body is edited after it has been checked. It also means every successful parse allocates a full copy of the structure, so on a hot path with a large payload the clone is real work that the 2 kB bundle size tells you nothing about. The README's other listed properties sit in the same family: zero external dependencies, a 2kb gzipped core bundle, an immutable API where methods return a new instance, and built-in JSON Schema conversion. That last one is the reason Zod often ends up in two places in a stack, at the edge where untrusted data arrives and in the contract generation that publishes the API's shape, which is also the case where the input and output types stop matching.
safeParse returns a discriminated union instead of throwing
The failure path has two shapes and picking the wrong one costs you. .parse() throws a ZodError instance, which means the exception crosses whatever frames your validation sits in. .safeParse() returns a plain result object holding either the parsed data or a ZodError, and the README notes that the result type is a discriminated union:
const result = Player.safeParse({ username: 42, xp: "100" });
if (!result.success) {
result.error; // ZodError instance
} else {
result.data; // { username: string; xp: number }
}Because the union is discriminated, TypeScript forces the failure branch to be handled rather than letting it fall through to code that assumes success, and the success branch narrows data to the inferred type. The field to build your own error format from is err.issues, an array where each entry carries the expected type, a code, the path into the object, and a message, which is enough to map validation failures onto the error shape your API already returns. In a request handler, safeParse is usually the right default. At a startup boundary where invalid configuration should abort the process, the throwing form reads better and fails louder.
z.infer, z.input and z.output are three different contracts
The static side is the reason the library is called TypeScript-first, and it has more than one exit. For a schema that validates without changing the shape, one utility is enough:
const mySchema = z.string().transform((val) => val.length);
type MySchemaIn = z.input<typeof mySchema>;
// => string
type MySchemaOut = z.output<typeof mySchema>; // equivalent to z.infer<typeof mySchema>
// numberA transform is what forces the split, because the value before parsing and the value after it are different types. z.input describes the shape you hand to the parser, z.output describes what comes back, and z.infer is equivalent to z.output. Consequence for a codebase with transforms in it: the type annotation on a variable holding unparsed data and the one holding parsed data are not interchangeable, and mixing them up is a compile error rather than a runtime surprise, which is the argument for the library. The corollary is that a transform is a design decision with a type-level footprint, since it changes what every downstream consumer sees. If a schema exists only to check a value, keeping it free of transforms keeps one type instead of two.
The repository still installs zod3 beside v4 for comparison
The root package.json is private and declares workspaces at packages/*, so the published manifest is not the one at the root. It also pins a package manager, nub, and devDependencies that describe how the project checks itself rather than what it ships. Two entries are the interesting ones: a workspace reference to zod itself, and an alias that installs the previous major from npm:
"zod": "workspace:*",
"zod3": "npm:zod@~3.24.0",So the v3 line is present in the tree as a dependency, which is what compatibility work and side-by-side benchmarking need. The rest of the list says what is verified before a release: recheck for property-based tests, tinybench and mitata alongside benchmark for measurement, @arethetypeswrong/cli for the published type entry points, @seriousme/openapi-schema-validator for generated schemas, arktype as a second implementation to compare against, and madge for import cycles. Formatting runs through biome with prettier on markdown, enforced by lint-staged and husky. Releases move quickly, with v4.6.3, v4.6.4 and v4.6.5 published on 2026-09-12 and 2026-09-13, and the last push to main on 2026-09-29.
Editorial conclusion
Choose Zod when you want one schema to drive both validation and the static type, when you can accept new Function as a runtime requirement or can set jitless, and when your hot paths parse structured objects large enough to pay for compilation. Do not choose it for a single z.string() check, where the README's own benchmark says compilation gains nothing, and think twice before compiling a schema whose refinements have side effects, since they run twice on invalid input. Verify three things before shipping: that a strict Content Security Policy does not block the generated code, that any async refine or transform has moved every call site to parseAsync or safeParseAsync, and that any schema you derived from a compiled one is compiled again yourself, because .refine() and .extend() return an uncompiled schema.
Frequently asked questions
What is zod and why do we use it?
Zod is a TypeScript-first validation library where you define a schema and parse data with it, getting back a strongly typed, validated result. It is listed as having zero external dependencies, a 2kb gzipped core bundle, an immutable API where methods return a new instance, and built-in JSON Schema conversion.
Can Zod be used with TypeScript?
That is the design centre of the library, and the README states it works with TypeScript and plain JS. A schema infers a static type you extract with z.infer, with z.input and z.output available separately when a transform makes the input and output types diverge.
What is Zod validation and how does it work?
You call .parse() with data and either get a strongly typed deep clone back or a thrown ZodError, or you call .safeParse() to receive a result object that is a discriminated union of the data or the error. On failure, err.issues carries the expected type, a code, the path and a message for each problem.
How to use zod?
Install it with npm install zod, define a schema such as z.object with your fields, and parse your input. Schemas using async refinements or transforms need .parseAsync() or .safeParseAsync() instead of the synchronous methods, and z.compile() gives a hot path an ahead-of-time compiled fast path.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/colinhacks-zod)