Development

Zod vs Yup: Which Schema Validator Should You Use in 2026?

Both libraries validate data at runtime — but they make very different trade-offs around TypeScript ergonomics, bundle size, and API design. Here's a practical breakdown to help you pick the right one for your project.

Last updated: July 2026  ·  13 min read

Schema validation is one of those corners of a codebase that looks boring until it breaks in production. An unvalidated API response quietly corrupts state; a malformed form field slips past a UI check and crashes a server handler. Getting validation right matters — and the library you reach for shapes how painful or pleasant that work feels.

Zod and Yup are the two most popular schema validation libraries in the JavaScript ecosystem. Yup has been around since 2016 and carries a huge installed base. Zod launched in 2020 with a TypeScript-first philosophy and has been closing the gap rapidly. In 2026, both are mature and well-maintained — but they suit different contexts.

This guide walks through every dimension that matters for a real project decision: TypeScript support, bundle cost, runtime performance, developer experience, form library integration, and community health. Code examples are included throughout so you can compare the APIs side by side.

At a Glance

If you only have two minutes, here's the summary. A deeper breakdown follows for each row.

DimensionZodYup
TypeScript inferenceBuilt-in, zero extra configRequires manual typing
Bundle size (minzipped)~13 kB~10 kB
Async validationSupportedFirst-class, older API
Error messagesStructured objectsString-based
React Hook FormOfficial @hookform/resolversOfficial @hookform/resolvers
Learning curveModerate (chain vs config)Shallow
npm weekly downloads~12 M+~8 M+
Best forTypeScript-first appsQuick JS projects, legacy codebases
Zod vs Yup: Which Schema Validator Should You Use in 2026?

TypeScript Integration

This is the biggest practical difference between the two libraries — and the reason Zod has pulled ahead in TypeScript-heavy teams.

Zod: types fall out of schemas for free

With Zod, you define a schema once and extract a TypeScript type from it using z.infer. There is no parallel type declaration to keep in sync:

import { z } from "zod";

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().min(18),
  role: z.enum(["admin", "editor", "viewer"]),
});

// TypeScript type is derived automatically — no duplication
type User = z.infer<typeof UserSchema>;

const result = UserSchema.safeParse(req.body);
if (result.success) {
  // result.data is fully typed as User
  console.log(result.data.email);
}

When the schema changes, the type changes with it. There's no risk of the two falling out of step.

Yup: types require a separate effort

Yup was written before TypeScript became the default in frontend development. Its types have improved over the years, but inference still requires more ceremony:

import * as yup from "yup";

const userSchema = yup.object({
  id: yup.string().uuid().required(),
  email: yup.string().email().required(),
  age: yup.number().integer().min(18).required(),
  role: yup.string().oneOf(["admin", "editor", "viewer"]).required(),
});

// You have to do this manually
type User = yup.InferType<typeof userSchema>;

// Yup throws on failure — you need try/catch
try {
  const data = await userSchema.validate(req.body, { abortEarly: false });
} catch (err) {
  if (err instanceof yup.ValidationError) {
    console.log(err.errors);
  }
}

Yup's InferType utility works, but edge cases — optional fields, unions, transforms — can produce inferred types that don't match what you expect. Zod's inference is more predictable partly because the library was designed with it in mind from day one.

Takeaway: If your project is TypeScript-first, Zod saves meaningful time and prevents a whole class of type-sync bugs. If you're working in plain JavaScript, this advantage disappears.

API Design and Developer Experience

Both libraries use a chainable, declarative API — but they feel different in practice.

Parsing vs validating

Zod makes a philosophical distinction: parsing returns a validated, possibly transformed value; validating just checks. This matches how schemas work in practice — you almost always want the transformed output, not just a yes/no answer.

  • schema.parse(data) — throws a ZodError on failure, returns typed data on success.
  • schema.safeParse(data) — always returns { success: true, data } | { success: false, error }. No try/catch needed, which makes API route handlers much cleaner.

Yup's default is async: schema.validate() returns a Promise and throws on failure. This is fine for forms, but adds boilerplate in synchronous contexts.

Error handling

Zod returns structured error objects. Each issue has a path, a code, and a human-readable message. This makes it trivial to map errors back to form fields:

const result = UserSchema.safeParse(badData);

if (!result.success) {
  // result.error.issues is an array of structured ZodIssue objects
  result.error.issues.forEach((issue) => {
    console.log(issue.path, issue.message, issue.code);
    // ["email"] "Invalid email" "invalid_string"
  });

  // Or get a flat field → message map
  const fieldErrors = result.error.flatten().fieldErrors;
}

Yup's errors are primarily string arrays. That's simpler for displaying a list of messages, but you lose the structured metadata if you need it later.

Transforms and preprocessing

Zod handles input coercion cleanly. Need to accept a string that should be treated as a number?

// Coerce a query param string to a number
const PageSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().max(100).default(20),
});

const params = PageSchema.parse({ page: "3", limit: "50" });
// { page: 3, limit: 50 }  ← fully typed numbers

Yup has .cast() for similar purposes, but the interaction between casting and validation can be surprising, especially with optional fields.

Form Validation: React Hook Form Integration

React Hook Form is the dominant form library in the React ecosystem, and it integrates with both Zod and Yup through the official @hookform/resolvers package. The setup is nearly identical.

Zod resolver

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const SignupSchema = z
  .object({
    email: z.string().email("Enter a valid email address"),
    password: z.string().min(8, "Password must be at least 8 characters"),
    confirm: z.string(),
  })
  .refine((data) => data.password === data.confirm, {
    message: "Passwords don't match",
    path: ["confirm"],
  });

type SignupData = z.infer<typeof SignupSchema>;

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<SignupData>({ resolver: zodResolver(SignupSchema) });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("email")} />
      {errors.email && <p>{errors.email.message}</p>}
      {/* ... */}
    </form>
  );
}

Yup resolver

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";

const signupSchema = yup.object({
  email: yup.string().email("Enter a valid email address").required(),
  password: yup.string().min(8, "Password must be at least 8 characters").required(),
  confirm: yup
    .string()
    .oneOf([yup.ref("password")], "Passwords don't match")
    .required(),
});

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({ resolver: yupResolver(signupSchema) });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("email")} />
      {errors.email && <p>{errors.email.message}</p>}
    </form>
  );
}

For pure form validation both resolvers work equally well. The Zod version gives you the inferred SignupData type for free, which flows into the useForm generic — smaller wins, but they add up across a large codebase.

Bundle Size and Performance

Neither library will dominate your bundle. Both are small enough that the decision should be driven by API fit, not byte count.

LibraryMinifiedMinzippedTree-shakeable
Zod v3~57 kB~13 kBPartial
Yup v1~40 kB~10 kBYes

Yup is slightly lighter and fully tree-shakeable, which can matter on heavily bandwidth-constrained pages. Zod v4 (in active development as of mid-2026) targets significant size reductions — watch the Zod GitHub releases if this is a deciding factor.

On runtime performance, both are fast enough for typical application validation. Zod's synchronous-first design gives it an edge in tight loops, while Yup's async model is better suited for validations that need to hit a database or external API.

Advanced Patterns

Discriminated unions (Zod wins clearly)

Modeling a payload that can take multiple shapes is clean in Zod:

const EventSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("created"), itemId: z.string() }),
  z.object({ type: z.literal("deleted"), itemId: z.string(), reason: z.string() }),
  z.object({ type: z.literal("updated"), itemId: z.string(), patch: z.record(z.unknown()) }),
]);

type AppEvent = z.infer<typeof EventSchema>;
// TypeScript now knows the exact shape when you narrow on .type

Yup can approximate this with .when(), but the TypeScript types don't narrow correctly, so you end up writing manual type guards anyway.

Cross-field validation

Both handle this, with slightly different APIs:

// Zod — use .superRefine() for complex cross-field logic
const BookingSchema = z
  .object({
    checkIn: z.coerce.date(),
    checkOut: z.coerce.date(),
  })
  .superRefine(({ checkIn, checkOut }, ctx) => {
    if (checkOut <= checkIn) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Check-out must be after check-in",
        path: ["checkOut"],
      });
    }
  });

// Yup — use .test() on the parent object
const bookingSchema = yup.object({
  checkIn: yup.date().required(),
  checkOut: yup
    .date()
    .min(yup.ref("checkIn"), "Check-out must be after check-in")
    .required(),
});

Yup's yup.ref() is concise for simple comparisons. Zod's .superRefine() is more verbose but gives you fine-grained control over which field the error attaches to.

Async validation

If you need to check uniqueness against a database during validation, both support async — but Yup's API was designed around it from the start:

// Yup async test
const registrationSchema = yup.object({
  username: yup
    .string()
    .required()
    .test("unique", "Username is already taken", async (value) => {
      if (!value) return true;
      const taken = await checkUsernameAvailability(value);
      return !taken;
    }),
});

// Zod async refinement
const RegistrationSchema = z.object({
  username: z.string(),
}).superRefine(async ({ username }, ctx) => {
  const taken = await checkUsernameAvailability(username);
  if (taken) {
    ctx.addIssue({ code: "custom", message: "Username is already taken", path: ["username"] });
  }
});
// Use schema.parseAsync(data) instead of schema.parse(data)

Server-Side and Full-Stack Use Cases

Zod has become the de-facto standard for full-stack TypeScript projects, largely because of its adoption by the tRPC ecosystem. If you're building with tRPC, Zod isn't optional — it's baked into the router API:

// tRPC router using Zod for input validation
export const appRouter = router({
  createPost: publicProcedure
    .input(
      z.object({
        title: z.string().min(1).max(200),
        body: z.string().min(10),
        tags: z.array(z.string()).max(5).optional(),
      })
    )
    .mutation(async ({ input }) => {
      // input is fully typed — title: string, body: string, tags?: string[]
      return db.post.create({ data: input });
    }),
});

For Next.js Route Handlers and Server Actions, both libraries work, but Zod's safeParse returns a clean discriminated union that pairs naturally with early-return API patterns:

// Next.js Route Handler
export async function POST(req: Request) {
  const body = await req.json();
  const result = CreatePostSchema.safeParse(body);

  if (!result.success) {
    return Response.json(
      { errors: result.error.flatten().fieldErrors },
      { status: 400 }
    );
  }

  const post = await db.post.create({ data: result.data });
  return Response.json(post, { status: 201 });
}

When to Use Zod

  • Your project is TypeScript-first and you want types derived from schemas automatically.
  • You're using tRPC, where Zod is the standard input validator.
  • You need discriminated unions or complex type narrowing.
  • You prefer synchronous validation with structured error objects.
  • You're validating API responses from third-party services and need runtime type safety.

When to Use Yup

  • You're working in a plain JavaScript codebase with no TypeScript.
  • You're adding validation to an existing project that already uses Yup — the migration cost isn't worth it.
  • Your team is familiar with Yup and async-first validation patterns suit your use case.
  • You need slightly smaller bundle output and full tree-shaking.
  • You're building forms with complex async field-level checks (e.g., real-time username availability).

Migrating from Yup to Zod

If you're starting a new project, pick one from the beginning. But if you're migrating an existing Yup codebase to Zod, here's a practical approach that avoids a big-bang rewrite:

  1. Install Zod alongside Yup. Both can coexist in the same project during the transition.
  2. Start with new schemas. Write all new validation in Zod, leaving existing Yup schemas in place.
  3. Migrate schema by schema. The API is similar enough that most schemas translate line-by-line. Pay special attention to async tests, which become.superRefine() in Zod.
  4. Update resolvers. Swap yupResolver for zodResolver as you migrate each form.
  5. Remove Yup when the last schema is gone. Don't rush this step — a gradual migration is safer than a full switch.

Community and Ecosystem Health in 2026

Both projects are actively maintained. Zod has surpassed Yup in weekly npm downloads and GitHub stars, reflecting the broader shift toward TypeScript-first tooling in the JavaScript ecosystem. Yup remains under active development and its maintainers have kept it well up to date — it's not a dead project.

The broader Zod ecosystem has grown substantially: there are community adapters for OpenAPI spec generation, JSON Schema conversion, Prisma integration, and more. If you expect to need those integrations, Zod's ecosystem is richer.

The Verdict

The practical answer for 2026 is: use Zod for new TypeScript projects, and stick with Yup if it's already working well in your codebase.

Zod's TypeScript inference is genuinely better — not marginally, but meaningfully. In a codebase with dozens of schemas, eliminating the parallel type declarations pays dividends every time something changes. The structured error objects, the clean safeParse pattern, and the discriminated union support all point in the same direction: Zod was designed for the way modern TypeScript applications are actually built.

That said, Yup isn't going anywhere. It's smaller, its async API is more ergonomic for certain patterns, and the migration cost from Yup to Zod is real. If a JavaScript project is already validated and tested with Yup, rewriting schemas to chase TypeScript inference is unlikely to be worth the effort.

Quick decision guide:

  • 🟢 New TypeScript project → Zod
  • 🟢 tRPC stack → Zod (non-negotiable)
  • 🟢 Complex union types needed → Zod
  • 🟡 Existing Yup codebase, JS project → keep Yup
  • 🟡 Heavy async field-level validation → either, slight Yup edge
  • 🔵 Bundle size is critical → Yup (marginally)

Frequently Asked Questions

Can I use Zod on the client and Yup on the server?

Technically yes, but it's a maintenance headache. You'll define equivalent schemas twice and keep them in sync manually. Pick one and use it throughout.

Is Zod suitable for validating environment variables?

Yes, and this is actually one of Zod's most popular use cases. Libraries like t3-env use Zod schemas to validate process.env at build time, surfacing missing variables before the app starts.

Does Zod work with JSON Schema?

Not natively, but the zod-to-json-schema package converts Zod schemas to standard JSON Schema format, enabling compatibility with OpenAPI generators and other tooling.

What about Valibot or Arktype?

Both are newer schema validation libraries worth watching. Valibot is notably tree-shakeable and extremely small. Arktype offers a unique string-literal schema syntax. Neither has matched Zod's ecosystem depth yet, but if bundle size is your primary constraint and you're starting fresh, Valibot in particular is worth evaluating.