Choosing between Zod, Yup, and Valibot affects more than how you write a schema: it changes how validation results are typed, how inputs are transformed, and what you need to consider for browser bundle size. This comparison explains their practical differences and how to choose for a TypeScript project.
Version note: I could not access live official documentation or package registries while preparing this article. Versions evaluated: none. The examples show commonly used APIs, not a guarantee of compatibility with the latest releases. Before adopting one, check its official documentation and the exact versions you plan to install; bundle-size comparisons especially require measuring your own build.
Key Takeaways
- Zod is a strong default when you want schemas closely integrated with TypeScript and a widely used API.
- Yup is worth considering when it fits an existing form-validation setup or your team already uses its casting and validation conventions.
- Valibot is designed around modular schemas and can be a good fit when small client bundles matter. Confirm the result with your own production build rather than assuming a size advantage.
- All three can validate data at runtime. TypeScript types alone do not validate form submissions, API responses, or other untrusted values.
- Compare input and output behavior, error handling, integrations, and measured bundle impact—not syntax alone.
The Core Difference: A Schema Is Runtime Code
A TypeScript type disappears at runtime. If an API response is malformed, a type annotation cannot detect it. A validation library provides executable rules and can report whether a value matches them.
The libraries also differ in what happens to valid-but-not-normalized input. A schema might trim a string, coerce a value, or transform it into another type. In those cases, it is important to know whether the type represents the original input or the parsed output.
The examples below illustrate a simple email schema. Their APIs are representative; check the documentation for your installed versions before copying them into a project.
Zod
import { z } from "zod";
const UserSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
});
type User = z.infer<typeof UserSchema>;
const result = UserSchema.safeParse({
email: "dev@example.com",
name: "Dev",
});
if (result.success) {
const user: User = result.data;
console.log(user.email);
} else {
console.error(result.error.issues);
}
Zod offers a schema-to-type workflow: infer a TypeScript type from the schema, and use parse or safeParse to validate at runtime. parse throws when validation fails; safeParse returns a success-or-error result instead.
Yup
import * as yup from "yup";
e const UserSchema = yup.object({
email: yup.string().email().required(),
name: yup.string().min(1).required(),
});
type User = yup.InferType<typeof UserSchema>;
async function validateUser(value: unknown): Promise<User> {
return UserSchema.validate(value, { abortEarly: false });
}
Yup validation commonly uses validate, which returns a promise and rejects with a validation error if the value fails. Yup schemas can also cast or transform values. That can be helpful for form inputs, but it means you should understand whether the returned value differs from what the user supplied. Options such as abortEarly affect how validation errors are collected.
Valibot
import * as v from "valibot";
const UserSchema = v.object({
email: v.pipe(v.string(), v.email()),
name: v.pipe(v.string(), v.minLength(1)),
});
type User = v.InferOutput<typeof UserSchema>;
const result = v.safeParse(UserSchema, {
email: "dev@example.com",
name: "Dev",
});
if (result.success) {
const user: User = result.output;
console.log(user.email);
} else {
console.error(result.issues);
}
Valibot builds many validations by piping a schema through actions such as string checks. Its inferred output type describes the parsed result. For schemas that transform data, distinguish that output from the input type using the library's input and output types.
Example correction: In the Yup example, the declaration should begin with
const UserSchema = ...; the strayebeforeconstis a typographical error and should not be copied.
Schema Definition and TypeScript Inference
All three libraries let you describe runtime rules and derive TypeScript types, but their styles differ.
- Zod puts the schema and inferred type close together. This is convenient when schemas are also used to validate data at application boundaries.
- Yup uses a fluent schema API and provides
InferTypefor deriving a type from a schema. Its approach can feel familiar in projects with established Yup-based forms. - Valibot composes schemas and validation actions. Its input and output types are useful when parsing changes a value.
A key design choice is which representation should be the source of truth. Deriving types from schemas reduces the chance that a separately maintained type drifts from runtime validation. If your project starts with an existing domain type, check how the library expresses schema input and parsed output rather than assuming they are interchangeable.
Transformations and Coercion
Validation and normalization are related but distinct. A validation rule answers whether data is acceptable; a transformation changes the value that continues through the application.
- Zod supports transforms in a schema pipeline. Its input and output types can differ when a transform changes the value.
- Yup supports casting and transforms as part of its schema behavior. Be explicit about coercion and strictness when the distinction matters—for example, whether a numeric string should be accepted as a number.
- Valibot supports transformations through its composable schema actions, with separate input and output typing available for transformed schemas.
For any library, test representative inputs such as an empty string, whitespace-only text, null, a numeric string, and an already-normalized value. Confirm both whether parsing succeeds and what value it returns. A form's raw input and an API's expected payload may need different schemas.
Error Handling
Error handling is part of the interface between validation and the rest of your application.
- Zod's
safeParseprovides a result object, with validation issues on failure. Useparsewhen throwing is the intended control flow. - Yup's
validatereturns a promise and reports failure through a validation error. When showing multiple field errors, configure and inspect error details appropriately. - Valibot's
safeParsereturns a result with issues when parsing fails.
Do not couple UI components tightly to a library's raw error structure without a reason. A small adapter can turn library-specific issues into the form your application needs, such as a field-path-to-message map. This makes switching libraries or changing how errors are displayed less disruptive.
Also decide whether users should see one error or several per field, how nested paths are presented, and whether messages need localization. Those requirements can matter more than small differences in schema syntax.
Ecosystem and Integrations
Before choosing, check the integrations your application actually uses: form libraries, API frameworks, shared schema packages, and any code-generation or documentation tooling. Confirm that the integration supports the versions you intend to install and handles errors and transforms in the way your application expects.
Yup may be a practical choice for a project that already has Yup schemas and working form integrations. Zod has a widely used schema-and-inference pattern. Valibot may suit a team that prefers composable validation actions. These are selection considerations, not guarantees that every integration is available or equally maintained for every version.
For a new project, prototype one representative workflow rather than choosing from API examples alone: validate a form, parse an API response, show nested field errors, and share a schema between client and server if that is a requirement.
Bundle Considerations
A library's package size is not the same as its impact on your production bundle. Actual results depend on the installed version, which parts of the API your code imports, your bundler and configuration, and whether schemas are included in client-side code.
Valibot is designed with modularity and small bundles in mind, but that is not a substitute for measurement. Do not rely on an unqualified package-size claim or an unrelated benchmark. Build a representative production page, inspect the emitted JavaScript with your normal tooling, and compare equivalent validation behavior under the same conditions. Keep server-only validation out of client bundles where your architecture allows it.
To record the package versions resolved in your own environment, inspect the lockfile or query the package registry when you have access. For example:
npm view zod version
npm view yup version
npm view valibot version
These commands query the registry at the time you run them; they do not establish which versions were used for this comparison.
A Practical Selection Workflow
- List the data boundaries. Identify forms, API requests, API responses, configuration, and other untrusted inputs that need runtime checks.
- Write down transformation rules. Decide whether inputs should be trimmed, coerced, or otherwise normalized, and what type downstream code should receive.
- Prototype errors and integrations. Test a nested object, multiple validation errors, and your actual form or server framework.
- Check package compatibility. Review the official documentation and package information for the versions your project will use. Record those versions in the decision so the comparison is reproducible.
- Measure if client size matters. Compare equivalent production builds, not library reputation or an unrelated benchmark.
- Choose for maintainability. Consider team familiarity, schema reuse, migration cost, and how easily the application can adapt errors.
If an application already uses one of these libraries successfully, switching purely for stylistic preference may not justify the migration and retesting work.
Common Mistakes
- Treating static types as runtime validation. Validate data that crosses a trust boundary; a TypeScript annotation does not check the received value.
- Ignoring parse output. A transform or cast can change the value. Use the parsed result, and type the output accordingly.
- Comparing versions that were not recorded. APIs, integrations, and package contents can change. Pin versions and consult the matching documentation.
- Choosing from bundle claims alone. Measure the production build that matters to your users.
- Assuming error formats are interchangeable. Map issues into an application-level format if multiple forms or services need consistent behavior.
- Running browser validation as the only check. Client-side validation improves feedback but does not replace validation on a server that accepts untrusted requests.
Best Practices
- Keep schemas near the boundary where data enters your application.
- Treat validated and transformed output as the trusted value, not the original input.
- Test valid cases, invalid cases, and edge cases involving empty or coerced values.
- Centralize error presentation when forms need consistent messages.
- Pin package versions and review official documentation when upgrading.
- Measure client bundle impact with your own production build when size is a deciding factor.
Conclusion
Zod, Yup, and Valibot can all validate data in TypeScript applications; the best fit depends on your existing ecosystem, transformation rules, error workflow, and client-side constraints. Prototype those requirements with the versions you plan to use, then choose based on behavior you have verified—not a blanket claim about syntax or bundle size.
