mrkeyoor.com_
Wed 05 Aug 05:04 UTC
npmUtilsupdated 05 Aug 2026

zod

Zod is a TypeScript-first schema validation library. You define a schema once (an object, a string with constraints, a union) and Zod gives you two things from that single definition: runtime validation of untrusted data and a static TypeScript type via z.infer. That one-source-of-truth trick is why it sits at the boundary of almost every serious TS codebase now: API inputs, env vars, form data, LLM outputs. Zero dependencies, works in Node and browsers, and since v4 it can also emit JSON Schema.

Verdict

The default choice for runtime validation in TypeScript, and deservedly so. Just check that everything in your stack speaks Zod 4 before upgrading, and reach for zod/mini or valibot when shipping validation to browsers.

API stability4/5Zod 4 (2025) was a real migration: error customization, .email() and friends moved to top-level, records changed. Within a major it is very stable, and v3 remains importable via a subpath.
Docs5/5zod.dev documents every API with runnable examples, has a dedicated v3-to-v4 migration guide, and the error customization docs cover the cases people actually hit.
Maintenance4/5Actively developed with pushes within the last week and regular releases, but it is substantially a single-maintainer project (colinhacks), which is a bus-factor risk at this level of adoption.
Ecosystem5/5The de facto schema layer for TypeScript: tRPC, react-hook-form resolvers, Hono validators, OpenAPI generators, and most AI SDKs accept Zod schemas directly.

Use it if

  • You have untrusted data crossing a boundary (HTTP body, env vars, webhook payloads, LLM JSON output) and want the parsed result to be a real TypeScript type, not a cast
  • You use tRPC, react-hook-form, or an OpenAPI generator; Zod is the default schema language for most of that ecosystem
  • You need to turn one schema definition into validation, TS types, and JSON Schema (z.toJSONSchema is built in as of v4)
  • You want zero dependencies and no build step or codegen
Skip it if

Setup reality

npm install zod and you are done; zero dependencies and no config. The real friction points: TypeScript strict mode is effectively required or inference degrades; the v3-to-v4 split means some ecosystem packages (form resolvers, OpenAPI generators) lag on v4 support, and you may end up importing the zod/v3 compatibility subpath to satisfy them; and a bare import of the full package is around 60 KB gzipped, so client-side code that cares should use zod/mini, which has a tree-shakeable functional API but different call syntax.

Patterns

Define a schema and parse untrusted dataobject-schema

import * as z from "zod";

const User = z.object({
  username: z.string().min(3),
  xp: z.number().int().nonnegative(),
});

const data = User.parse(input); // throws ZodError on bad input
// data is typed { username: string; xp: number }

parse returns a validated deep clone of the input, not the original object; unknown keys are stripped by default.

Validate without try/catchsafe-parse

const result = User.safeParse(input);
if (!result.success) {
  console.log(result.error.issues); // array of { path, code, message }
} else {
  result.data; // typed and validated
}

The result is a discriminated union; TypeScript narrows data/error correctly after the success check.

Extract the TypeScript type from a schemainfer-types

const User = z.object({ username: z.string(), xp: z.number() });
type User = z.infer<typeof User>;

// when transforms change the type, input and output differ:
const S = z.string().transform((v) => v.length);
type In = z.input<typeof S>;   // string
type Out = z.output<typeof S>; // number

z.infer is z.output; use z.input for the pre-transform shape (matters for form libraries).

Validate email, URL, UUID (Zod 4 style)string-formats

const Contact = z.object({
  email: z.email(),
  site: z.url(),
  id: z.uuid(),
});

In Zod 4 these are top-level functions; the old z.string().email() chain is deprecated.

Coerce env vars and query stringscoercion

const Env = z.object({
  PORT: z.coerce.number().int().default(3000),
  DEBUG: z.stringbool().default(false),
});

const env = Env.parse(process.env);

z.coerce.boolean() treats any non-empty string (including "false") as true; use z.stringbool() for real "true"/"false" parsing.

Customize error messagescustom-errors

const Password = z
  .string({ error: "Password is required" })
  .min(8, { error: "At least 8 characters" });

Zod 4 unified message, invalid_type_error and errorMap into a single error param; the old names still work but log deprecation in types.

Cross-field validation with refinerefine

const Signup = z
  .object({ password: z.string(), confirm: z.string() })
  .refine((d) => d.password === d.confirm, {
    error: "Passwords do not match",
    path: ["confirm"],
  });

Set path so the issue lands on the right field; async refinements force you onto parseAsync/safeParseAsync.

Parse one of several shapes by a tag fielddiscriminated-union

const Event = z.discriminatedUnion("type", [
  z.object({ type: z.literal("click"), x: z.number(), y: z.number() }),
  z.object({ type: z.literal("keypress"), key: z.string() }),
]);

Much faster and much better error messages than z.union when the variants share a literal tag.

Recursive types (categories with children)recursive-schema

const Category = z.object({
  name: z.string(),
  get children() {
    return z.array(Category);
  },
});
type Category = z.infer<typeof Category>;

Zod 4 supports recursion via getters with full inference; the old z.lazy plus manual type annotation dance is no longer needed.

Convert a schema to JSON Schemajson-schema

const User = z.object({ name: z.string(), age: z.number() });
const jsonSchema = z.toJSONSchema(User);
// feed to OpenAPI, LLM structured output, form generators

Built into Zod 4, no zod-to-json-schema package needed; transforms and custom refinements cannot be represented and throw unless you set io or override options.

Shape errors for a form UIflatten-errors

const result = Signup.safeParse(formData);
if (!result.success) {
  const tree = z.treeifyError(result.error);
  // tree.properties?.password?.errors -> string[]
}

error.flatten() and error.format() are deprecated in v4; z.treeifyError and z.flattenError are the replacements.

Alternatives

PackageRegistryPick it when
valibotnpmClient-side validation where bundle size matters; modular design tree-shakes to about 1 KB for simple schemas
arktypenpmYou want schemas written as TypeScript-like string syntax and faster runtime validation
yupnpmA legacy Formik codebase already uses it; do not pick it fresh, its TS inference is weaker