Library / SDK
colinhacks/zod avatar
colinhacks/zod

Zod: schema validation that hands the TypeScript type back to you

TypeScript-first schema validation with static type inference

43,954 stars2,190 forksTypeScriptMIT

At a glance

What is it?
Zod defines a runtime validator and a static type from one declaration. The v4 compile path is the interesting part, and it has boundaries worth reading before you turn it on globally.
Who is it for?
Adopt Zod when a single declaration must produce both the runtime check and the TypeScript type, and when the boundary you validate is a JSON-shaped payload rather than a stream of binary frames. Do not adopt it if you need a compiled JSON Schema artifact as the primary output, or if your schemas are dominated by async refinements and transforms, since those are exactly what the compile path declines to handle.
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 last received commits 1 day ago.
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 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The duplication Zod removes

The problem is not validation on its own. It is that a TypeScript interface describes data that arrives at runtime, and an interface erases to nothing. So teams write the interface once and the validator a second time, and the two drift until a field is renamed in one place only. Zod's answer is a single declaration that produces both. The README opens with an object schema built from z.string() and z.number(), then shows z.infer<typeof Player> extracting the static type from that same value. One source, two consumers. That is the whole pitch, and it is aimed at anyone validating a boundary: an HTTP request body, a config file, a message off a queue. The library ships with zero external dependencies, works in Node.js and modern browsers, and the README puts the core bundle at 2kb gzipped, which is small enough that the size argument rarely decides the question either way.

Parse returns a deep clone, not your object

The mechanism is worth stating precisely because it changes how you write call sites. Schema.parse(input) validates and returns a strongly typed deep clone of the input. It is not the same reference. If you parse a request body and then mutate the result, you are mutating a copy, and the original object is untouched. The README is explicit that the return is a clone, though it does not spell out the aliasing consequences, so treat that as something to confirm against the docs rather than assume. A second consequence: parse throws. When validation fails you get a ZodError whose issues array carries objects with expected, code, path and message, so a two-field failure reports both fields with their paths rather than stopping at the first. If try/catch does not suit the call site, safeParse returns a discriminated union instead, and TypeScript narrows on result.success to give you either result.data or result.error. The async variants exist for a reason: a schema containing an async refinement or transform must be driven with parseAsync or safeParseAsync, and the README repeats that note in both the parse and safeParse sections. Mixing them up is a runtime problem, not a type error.

z.compile and the numbers the README publishes

Version 4 added ahead-of-time compilation. z.compile(schema) returns a schema clone carrying a compiled fast path. Valid inputs take that path; invalid inputs fall back to the ordinary parser so error reporting is unchanged. The README reports a median 2.4x speedup across a 55-schema benchmark, and it is unusually honest about where the gain comes from: a large array of objects is about 9x, a 20-key object about 9x, a nested object about 4.5x, and a bare z.string() gains nothing, because compilation removes per-node dispatch and allocation and a single typeof has none of either to remove. Those figures are the project's own, measured on its own benchmark, and I have not reproduced them. The shape of the claim is what matters: the benefit scales with how much work each parse does. Global mode is opt-in through a side-effect import, import "zod/compile", placed before the modules that define schemas. Two constraints come with it. Compilation uses new Function, so global mode is automatically disabled when z.config({ jitless: true }) is set, which the README names as the CSP case. And schemas with async refinements or transforms cannot be compiled, along with a few other constructs. That is not treated as an error: z.compile() returns the schema unchanged and it keeps using the regular parser. Pass { strict: true } and it throws ZodCompileAsyncError or ZodCompileUnsupportedError instead.

Three compile behaviours that will bite quietly

The first is double execution. On invalid input, refinements and transforms may run twice, once on the fast path and once on the fallback. If a refinement has a side effect, a log line, a counter increment, a cache write, it can fire twice for the same rejected payload. The README states this plainly; it is easy to miss. The second is derivation. Refining or extending a compiled schema returns an uncompiled schema, so compile the final schema, not an intermediate one. A pipeline that builds a base schema, extends it per endpoint, then refines it has to call z.compile at the end or the fast path is gone. The third is the fallback itself. Because unsupported schemas pass through unchanged rather than failing loudly, a codebase can believe it is running compiled while a subset of schemas is not. Strict mode is the way to find out, and it is worth a one-off pass rather than leaving it on. None of these make the feature wrong; they make it a thing you verify rather than assume.

What Zod is not the right tool for

The README lists built-in JSON Schema conversion and an extensive ecosystem, but the primary artifact is a TypeScript schema, not a JSON Schema document. If your schemas are authored elsewhere and consumed by non-TypeScript services, Zod is a downstream convenience rather than the source of truth. The compile path is the sharper limitation. A validation layer built around async refinements, checking a database for uniqueness, calling an external service, will never compile, and the README says so directly. In that case you are paying the schema-authoring cost and getting the regular parser. The other boundary is the environment. new Function is unavailable under a strict Content-Security-Policy, and the README's answer is z.config({ jitless: true }) or calling z.compile explicitly as an opt-in. A team that turns on global compilation and later tightens its CSP will find the fast path gone with no build error. Nothing in the README describes a streaming or binary decoder, so for a protocol of framed binary messages, a schema library of this shape is the wrong layer.

Valibot and the bundle-size argument

Valibot is the closest alternative and the difference is architectural rather than cosmetic. Valibot is built from small, individually importable functions so that a bundler can tree-shake down to the validators actually used, which matters when the schema is shipped to the browser. Zod's README quotes a 2kb gzipped core, a fixed floor you pay whether you use three validators or thirty. Those are different bets: Zod trades a constant overhead for a fluent, method-chained API where every schema is an object you can pass around and extend, while Valibot trades that fluency for a smaller surface. Zod also carries the compile path, which Valibot's model does not have in the same form. If you are validating on a server, the 2kb floor is irrelevant and the compile numbers are the ones that matter. If you are validating in a browser bundle on a slow connection, measure both against your actual schema set before deciding.

Upgrades, licence and what to check first

Zod is MIT licensed, which permits commercial use, modification and redistribution provided the copyright notice and permission notice are retained. That is a plain permissive licence and I am not giving legal advice; read the LICENSE file in the repository for the operative text. On maintenance, the release history shows v4.6.1 on 2026-09-09, one day after v4.6.0, and v4.5.4 about two weeks earlier, with the last push to main on 2026-09-10. Patch releases arriving that close together suggest an active line, though I cannot tell from the README what changed in any of them. The upgrade cost that matters is the v4 compile path, because it introduces a global configuration surface through z.config and a side-effect import that has to be ordered before schema definitions. The thing to verify first is narrow: take your heaviest schemas, call z.compile on each with { strict: true }, and see which throw. That single run tells you how much of your validation layer can actually use the fast path, and it takes less time than reading this article.

Editorial conclusion

Adopt Zod when a single declaration must produce both the runtime check and the TypeScript type, and when the boundary you validate is a JSON-shaped payload rather than a stream of binary frames. Do not adopt it if you need a compiled JSON Schema artifact as the primary output, or if your schemas are dominated by async refinements and transforms, since those are exactly what the compile path declines to handle. Before committing, run z.compile on your two or three heaviest schemas with { strict: true } to see which ones throw ZodCompileAsyncError or ZodCompileUnsupportedError, and check whether your deployment sets a Content-Security-Policy that forbids new Function, because global compilation is silently disabled in that case.

Official sources

  1. colinhacks/zod on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Community notes

Community notes