Validate JSON in Node.js with Ajv
Compile reusable JSON Schema validators with Ajv, validate formats, define custom keywords, and return useful errors without hiding schema problems.
An HTTP request can contain valid JSON and still be unusable: a required field is missing, an age arrives as a string, or a client sends a property your API does not accept. Parsing the request only establishes that it is JSON. You still need to check its structure before passing it to application code.
Ajv handles that second step with JSON Schema. You describe the expected data, compile the schema into a JavaScript validation function, and reuse that function for subsequent requests. This guide uses the Ajv 8 API and covers format validation, custom rules, and error handling.
When Ajv fits
Ajv is a useful choice when JSON Schema is already part of your API contracts or when several services need to share a schema. Fastify also uses Ajv for its request-validation system, although the framework's configuration can differ from the standalone setup below.
The distinction from libraries such as Zod and Joi is the schema format. Ajv works with JSON Schema; Zod emphasizes TypeScript inference, while Joi provides a JavaScript schema-building API. If you need a portable JSON Schema contract, Ajv is a natural starting point. If most of your validation lives inside a TypeScript application, consider how much value you get from inferred types and the surrounding tooling.
Ajv generates validation code when you compile a schema. Compilation has a cost, so compile a stable schema during initialization and retain the resulting function. Compiling it inside every request handler defeats the purpose of that approach.
Install Ajv and compile a validator
Install Ajv and its standard format plugin:
npm install ajv ajv-formats
The following CommonJS example validates an object with a name, an email address, and an optional age:
const Ajv = require('ajv');
const addFormats = require('ajv-formats');
const ajv = new Ajv({ allErrors: true });
addFormats(ajv); // Register formats such as email, uri, and date-time.
const schema = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 }
},
required: ['name', 'email'],
additionalProperties: false
};
const validate = ajv.compile(schema);
const ok = validate({ name: 'Guance', email: 'a@b.com' });
if (!ok) console.log(validate.errors);
The call returns true when the data passes validation and false otherwise. For a failed call, validate.errors contains the validation errors.
Three details are easy to overlook. Listing a property under properties does not make it mandatory; that is what required does. additionalProperties: false rejects properties that the schema does not allow. It does not remove them unless you separately enable an option that changes the input data. Finally, allErrors: true collects all validation errors rather than stopping at the first one. That can be helpful for a form where the user should see several problems at once.
This setup does not enable type coercion. An input of "42" will therefore fail the integer check for age, even though 42 passes it. Decide whether your API should reject or normalize that input, and make the behavior explicit.
Express the constraints your application actually needs
JSON Schema provides keywords for common constraints:
| Data or condition | Useful keywords | What to check |
|---|---|---|
| Strings | minLength, maxLength, pattern, format |
Length, an application-specific pattern, or a registered format |
| Numbers | minimum, maximum, exclusiveMinimum, multipleOf |
Allowed bounds and increments |
| Arrays | items, minItems, uniqueItems |
Element structure, minimum size, and duplicate values |
| Allowed values | enum, const |
Membership in a fixed set or equality to one value |
| Combined conditions | anyOf, oneOf, allOf |
At least one, exactly one, or all subschemas must match |
| Conditional validation | if, then, else |
Apply a subschema when a condition matches |
The difference between anyOf and oneOf matters. With oneOf, matching two alternatives is a validation failure, even if either alternative would be acceptable by itself. Use it when the alternatives are intended to be mutually exclusive.
Keep domain rules separate when doing so makes them easier to understand and test. Checking that an end time follows a start time may be clearer in application code after structural validation than in a complicated schema. A database uniqueness check also has different failure and concurrency behavior from a local shape check.
Add a custom keyword when standard keywords are not enough
Ajv supports custom keywords through addKeyword. This example requires a string consisting of 1 followed by ten digits. It is a simplified shape check for a mainland China mobile number, not a complete phone-number validator or proof that the number exists.
ajv.addKeyword({
keyword: 'isPhone',
type: 'string',
schemaType: 'boolean',
validate: (enabled, data) => !enabled || /^1\d{10}$/.test(data),
errors: false
});
const phoneSchema = { type: 'string', isPhone: true };
const validatePhone = ajv.compile(phoneSchema);
console.log(validatePhone('13800138000')); // true
console.log(validatePhone('123')); // false
The type declaration restricts the keyword to string data, and schemaType declares the type of its schema value. The validator also handles isPhone: false explicitly. With errors: false, Ajv creates a standard error when the keyword fails; map that error to the wording your users need.
For this simple rule, pattern would be sufficient. A custom keyword becomes useful when the rule is reused across many schemas or requires logic that the standard keywords do not express clearly. Remember that another JSON Schema implementation will not automatically know what your custom keyword means.
Turn validation errors into useful responses
Ajv errors include an instancePath, the failed keyword, a message, and keyword-specific params. Use those fields to build your application's error response instead of sending an unprocessed stack trace or echoing the entire request body.
A missing required property needs special attention: instancePath identifies the object being checked, while params.missingProperty identifies the missing field. For other failures, the path often points directly to the offending value. A formatter should account for that distinction rather than assuming every error contains a field name in the same place.
Read or copy validate.errors immediately after the failed validation call. A later call to the same validator replaces that result. If you prefer to define messages alongside the schema, the separate ajv-errors plugin supports customized error messages and requires allErrors: true.
Operationally, a rise in validation failures can signal a client rollout, a contract change, or unexpected input. Record the endpoint, failed rule, and appropriate request context so you can investigate the pattern without routinely logging sensitive payload values. If those logs are collected in Guance, consistent service and request identifiers make the failures easier to investigate alongside other application events.
Troubleshoot format and strict-mode errors
Why is format: 'email' failing at schema compilation?
Ajv 7 and later moved the standard JSON Schema formats into ajv-formats. Install the plugin and call addFormats(ajv) before compiling a schema that uses them. With the default strict behavior, an unknown format causes a compilation error. Disabling strict mode can cause an unknown format to be ignored; it does not install the missing validator. Email format validation checks syntax, not whether the mailbox exists or belongs to the user.
Should I set strict: false to make a schema work?
Ajv supports new Ajv({ strict: false }), but first identify the condition that produced the error. Strict mode catches ambiguous or unsupported schema usage that can otherwise be ignored. Fixing a misspelled keyword or an incorrect schema is usually preferable to suppressing the check across the application.
Can I use the same schema with OpenAPI?
Check the OpenAPI version and the JSON Schema dialect your tooling supports. Sharing schemas can reduce duplication, but compatibility is not automatic across all OpenAPI and JSON Schema versions. Choose the matching Ajv configuration before treating a schema as a shared contract.
Once the validator is in place, test valid inputs, missing fields, unexpected fields, wrong types, and boundary values. Those cases establish the contract your callers can depend on and make later schema changes much easier to review.
References
- Ajv: Getting started
- Ajv: Format validation
- Ajv: User-defined keywords
- Ajv: Strict mode
- Ajv errors plugin
This guide is based on the projects' documentation. The examples target Ajv 8; it does not present a performance benchmark.
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan