ajv, class-validator, joi, yup, and zod are libraries used to validate data structures at runtime. They ensure that incoming data matches expected shapes before your application processes it. While ajv follows the official JSON Schema standard, joi, yup, and zod use builder patterns to define rules. class-validator relies on TypeScript decorators attached to class properties. These tools help prevent bugs caused by invalid data in forms, API responses, and configuration files.
Validating data is a core part of building reliable web applications. Whether you are checking form inputs or verifying API responses, you need tools that ensure data matches your expectations. ajv, class-validator, joi, yup, and zod all solve this problem, but they approach it from different angles. Let's compare how they handle schema definition, TypeScript integration, and error handling.
The way you define what valid data looks like varies significantly across these libraries.
ajv uses the official JSON Schema standard.
// ajv: JSON Schema object
const schema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" }
},
required: ["name", "age"]
};
class-validator uses TypeScript decorators on class properties.
// class-validator: Decorator class
import { IsString, IsNumber } from "class-validator";
class User {
@IsString()
name: string;
@IsNumber()
age: number;
}
joi uses a chainable builder API.
.string() and .required() to build rules.// joi: Chainable schema
const schema = Joi.object({
name: Joi.string().required(),
age: Joi.number().required()
});
yup also uses a chainable builder API, similar to joi.
// yup: Chainable schema
const schema = yup.object({
name: yup.string().required(),
age: yup.number().required()
});
zod uses a chainable builder API focused on TypeScript.
z.string() and z.object().// zod: Chainable schema
const schema = z.object({
name: z.string(),
age: z.number()
});
Type safety is a major concern for frontend architects. Some libraries help you avoid writing types twice.
ajv does not infer TypeScript types.
// ajv: Manual type definition
interface User {
name: string;
age: number;
}
// Schema is separate and not linked to interface
class-validator uses the class definition as the type.
// class-validator: Class as type
function validate(user: User) {
// User type comes from the class
}
joi does not infer TypeScript types natively.
// joi: Manual type definition
interface User {
name: string;
age: number;
}
// Schema validation is runtime only
yup supports type inference but requires extra steps.
InferType from yup to get a TypeScript type from the schema.// yup: Type inference
const schema = yup.object({ name: yup.string() });
type User = yup.InferType<typeof schema>;
zod infers TypeScript types automatically.
z.infer to extract the type directly from the schema.// zod: Automatic type inference
const schema = z.object({ name: z.string() });
type User = z.infer<typeof schema>;
Once schemas are defined, you need to check data against them.
ajv compiles schemas into validation functions.
// ajv: Compile and validate
const ajv = new Ajv();
const validate = ajv.compile(schema);
const valid = validate(data); // true or false
class-validator uses a standalone validate function.
// class-validator: Validate instance
const user = new User();
const errors = await validate(user);
joi uses the .validate() method on the schema.
error property to see if validation failed.// joi: Validate method
const { error, value } = schema.validate(data);
yup uses the .validate() method which returns a promise.
// yup: Async validate
try {
const value = await schema.validate(data);
} catch (err) {
// Handle error
}
zod uses .parse() or .safeParse().
.parse() throws an error if invalid..safeParse() returns an object with success status, avoiding try/catch.// zod: Safe parse
const result = schema.safeParse(data);
if (result.success) { /* use result.data */ }
When validation fails, you need clear error messages to show users.
ajv stores errors on the instance after validation.
validate.errors to see what went wrong.// ajv: Error collection
if (!validate(data)) {
console.log(validate.errors);
}
class-validator returns an array of error objects.
// class-validator: Error array
const errors = await validate(user);
errors.forEach(e => console.log(e.constraints));
joi provides a detailed error object.
// joi: Error details
if (error) {
console.log(error.details[0].message);
}
yup throws a ValidationError with path information.
path and message.// yup: Catch error
try {
await schema.validate(data);
} catch (err) {
console.log(err.path, err.message);
}
zod returns formatted errors in the result object.
safeParse gives you a error.format() method for easy access.// zod: Format errors
if (!result.success) {
console.log(result.error.format());
}
| Feature | ajv | class-validator | joi | yup | zod |
|---|---|---|---|---|---|
| Schema Style | JSON Schema Object | Class Decorators | Chainable Builder | Chainable Builder | Chainable Builder |
| Type Inference | ❌ Manual | ✅ Via Class | ❌ Manual | ✅ Via Helper | ✅ Built-in |
| Validation Style | Compiled Function | Class Instance | Method .validate() | Async .validate() | .parse() / .safeParse() |
| Error Output | Instance Property | Error Array | Error Object | Thrown Error | Result Object |
| Dependencies | Few | Reflect Metadata | None | None | Zero |
Choosing the right library depends on your project's architecture and type safety needs.
ajv is the best choice for strict JSON Schema compliance and high-performance validation in large-scale systems. It is ideal when interoperability with other tools is a priority.
class-validator fits best in backend-heavy or full-stack TypeScript projects using NestJS where class-based models are already standard.
joi remains a solid option for legacy Node.js projects but is less common in modern frontend bundles due to size and lack of type inference.
yup is a reliable choice for React forms, especially if you are already using Formik. It offers stability and a mature ecosystem.
zod is the top recommendation for new TypeScript projects. Its automatic type inference reduces boilerplate and keeps your runtime checks in sync with your compile-time types. It offers the best balance of safety and developer experience for modern frontend development.
Choose ajv if you need strict compliance with the JSON Schema standard or require maximum validation speed for large datasets. It is ideal when sharing schemas between frontend and backend systems that already use JSON Schema. Be aware that it does not infer TypeScript types automatically.
Choose class-validator if your project uses a class-based architecture, such as NestJS, and relies heavily on decorators. It works well when you want validation logic attached directly to your data models. Note that it requires specific TypeScript compiler settings to enable decorators.
Choose joi if you are maintaining a legacy project that already depends on it or need a very mature feature set for complex Node.js backends. It is less common in modern frontend bundles due to size concerns. Consider lighter alternatives for new client-side projects.
Choose yup if you are using Formik or need a stable library with a long history of React form integration. It offers a chainable API that is easy to read and modify. It is a solid choice for teams that prioritize stability over cutting-edge TypeScript features.
Choose zod if you want TypeScript type inference directly from your validation schemas. It has zero dependencies and provides a modern developer experience with minimal setup. It is increasingly becoming the default choice for new TypeScript projects.
The fastest JSON validator for Node.js and browser.
Supports JSON Schema draft-04/06/07/2019-09/2020-12 (draft-04 support requires ajv-draft-04 package) and JSON Type Definition RFC8927.
More than 100 people contributed to Ajv, and we would love to have you join the development. We welcome implementing new features that will benefit many users and ideas to improve our documentation.
Please review Contributing guidelines and Code components.
All documentation is available on the Ajv website.
Some useful site links:
Since I asked to support Ajv development 40 people and 6 organizations contributed via GitHub and OpenCollective - this support helped receiving the MOSS grant!
Your continuing support is very important - the funds will be used to develop and maintain Ajv once the next major version is released.
Please sponsor Ajv via:
Thank you.
Ajv generates code to turn JSON Schemas into super-fast validation functions that are efficient for v8 optimization.
Currently Ajv is the fastest and the most standard compliant validator according to these benchmarks:
Performance of different validators by json-schema-benchmark:
addSchema or compiled to be available)type keywordsTo install version 8:
npm install ajv
Try it in the Node.js REPL: https://runkit.com/npm/ajv
In JavaScript:
// or ESM/TypeScript import
import Ajv from "ajv"
// Node.js require:
const Ajv = require("ajv")
const ajv = new Ajv() // options can be passed, e.g. {allErrors: true}
const schema = {
type: "object",
properties: {
foo: {type: "integer"},
bar: {type: "string"},
},
required: ["foo"],
additionalProperties: false,
}
const data = {
foo: 1,
bar: "abc",
}
const validate = ajv.compile(schema)
const valid = validate(data)
if (!valid) console.log(validate.errors)
Learn how to use Ajv and see more examples in the Guide: getting started
See https://github.com/ajv-validator/ajv/releases
Please note: Changes in version 8.0.0
Please review and follow the Code of conduct.
Please report any unacceptable behaviour to ajv.validator@gmail.com - it will be reviewed by the project team.
To report a security vulnerability, please use the Tidelift security contact. Tidelift will coordinate the fix and disclosure. Please do NOT report security vulnerabilities via GitHub issues.
Ajv is a part of Tidelift subscription - it provides a centralised support to open-source software users, in addition to the support provided by software maintainers.