Migrating from ajv
Your JSON Schemas stay as they are: schiva reads draft-04, draft-06, draft-07, 2019-09 and 2020-12, and passes the whole JSON-Schema-Test-Suite of each. What changes is the code around them: how you compile, what the function returns and how errors look. This page goes through all of it, with the few differences that can change a result.
Three ways to move
schiva is not a drop-in replacement: the schemas are the same, the API is not. Pick the path that fits:
| If... | Do this |
|---|---|
You call ajv.compile() in a few places | Move directly: replace it with compileJsonSchema() and read the errors it returns. Start with Side by side and follow the checklist. |
Much code reads validate.errors and instancePath | Use the ajv-compatible adapter (about 40 lines to copy): the same compile(), validate.errors and error objects, then move the code over little by little. |
| You use Fastify | Set schiva as its validator compiler, with Fastify's own defaults. |
In every case, run both libraries on your schemas and test data first: it takes a few lines and shows whether anything behaves differently for you.
Why move
- Speed. About twice as fast as ajv validating (2.1× on the JSON Schema test suite, 2.2× on realistic payloads), and 44× faster compiling. See the benchmarks.
-
Correctness. schiva passes all 4,950 tests of the JSON-Schema-Test-Suite; ajv passes 4,836.
Most of ajv's failures are in 2019-09 and 2020-12 (
unevaluatedProperties,$dynamicRef, references). -
Readable errors. Messages name the field:
lines[0].price must be at least 0instead ofmust be >= 0next to aninstancePath. -
One package. Formats, ajv-keywords, draft-04,
discriminatorwith OpenAPI'smapping, and standalone code are built in. No dependencies.
Install
npm install schiva
npm uninstall ajv ajv-formats ajv-keywords ajv-draft-04 # once nothing uses them
| ajv import | schiva |
|---|---|
import Ajv from 'ajv' (draft-07) | import { compileJsonSchema } from 'schiva', or const { compileJsonSchema } = require('schiva'). One function for every draft. |
import Ajv2019 from 'ajv/dist/2019' | |
import Ajv2020 from 'ajv/dist/2020' | |
import Ajv from 'ajv-draft-04' | |
import addFormats from 'ajv-formats' | the option formats |
import ajvKeywords from 'ajv-keywords' | import { ajvKeywords } from 'schiva' |
import standaloneCode from 'ajv/dist/standalone' | import { standaloneJsonSchema } from 'schiva' |
import type { JSONSchemaType, ErrorObject } from 'ajv' | import type { JsonSchema, ErrorObject } from 'schiva' (see TypeScript) |
What changes
-
No instance. ajv compiles through an
Ajvobject that holds options, formats and schemas. schiva compiles with a function,compileJsonSchema(schema, options), and the options go with each call. See Replacing the instance. -
Errors are returned. An ajv function returns
trueorfalseand leaves the errors invalidate.errors. A schiva function returns the errors: messages by default, objects witherrors: 'objects', ortrue/falsewitherrors: false. Nothing is kept between calls, so it is safe with concurrent code. -
Every error by default. ajv stops at the first error unless
allErrors: true. schiva reports every error unlessallErrors: false. -
The draft comes from
$schema, else draft-07.Ajv2019andAjv2020assume their draft; with schiva, schemas without$schemaneeddraft: '2020-12'(or'2019-09'). -
Strict. An unknown keyword throws when compiling, as in ajv's strict mode, and so does a
keyword the
typeexcludes (ajv only warns withstrictTypes). Declare your own annotations withkeywords, or ignore them withstrict: false. -
The data is not changed unless you ask:
useDefaults,removeAdditionalandcoerceTypeswork as in ajv.
Side by side
const Ajv2020 = require('ajv/dist/2020');
const addFormats = require('ajv-formats');
const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
ajv.addSchema(addressSchema, 'https://example.com/address.json');
const validate = ajv.compile(orderSchema);
app.post('/orders', (req, res) => {
if (!validate(req.body)) {
return res.status(400).json({ errors: validate.errors });
}
// ...
});
const { compileJsonSchema } = require('schiva');
const validate = compileJsonSchema(orderSchema, {
draft: '2020-12', // or "$schema" in the schema
formats: true,
schemas: { 'https://example.com/address.json': addressSchema },
errors: 'objects', // leave it out for messages
});
app.post('/orders', (req, res) => {
const errors = validate(req.body);
if (errors.length > 0) {
return res.status(400).json({ errors });
}
// ...
});
Replacing the instance
An Ajv instance does three things: it holds shared options and schemas, it caches compiled
functions, and it finds schemas by $id. A few lines of your own do the same:
// validation.js: shared by the whole application
const { compileJsonSchema } = require('schiva');
const options = { draft: '2020-12', formats: true, schemas: [addressSchema, productSchema] };
// Like ajv.compile(): compiled once per schema object, then reused.
const cache = new WeakMap();
function compile(schema) {
if (!cache.has(schema)) {
cache.set(schema, compileJsonSchema(schema, options));
}
return cache.get(schema);
}
// Like ajv.getSchema(id): a schema that only references the one with that "$id".
const getSchema = (id) => compileJsonSchema({ $ref: id }, options);
module.exports = { compile, getSchema };
The cache matters if your code called ajv.compile() or ajv.validate() on each
request and relied on ajv's cache: compileJsonSchema() compiles every time it is called. Compile
when the program starts, or cache as above.
API
| ajv | schiva |
|---|---|
new Ajv(options), Ajv2019, Ajv2020, ajv-draft-04 | no instance; the options go to each compileJsonSchema(); the draft comes from $schema, or the option draft |
ajv.compile(schema) | compileJsonSchema(schema, options) |
validate(data) → true/false | validate(data) → true/false, compiled with errors: false |
validate.errors (null when valid) | what the function returns: [] when valid |
ajv.errorsText(validate.errors) | errors.join(', '): messages already name the field |
ajv.validate(schema, data) | compile once and call the function (see Replacing the instance) |
ajv.compileAsync(schema) with loadSchema | compileJsonSchemaAsync(schema, { loadSchema }) (see Several documents) |
ajv.addSchema(schema, uri) | the option schemas: { uri: schema }, or [schema] with $id |
ajv.getSchema(id) | compileJsonSchema({ $ref: id }, { schemas }) |
ajv.removeSchema(), ajv.addUsedSchema | nothing is registered globally: pass the schemas you want to each call |
ajv.validateSchema(schema), ajv.addMetaSchema() | compile the meta-schema, registered in schemas (see below) |
ajv.addFormat(name, format), ajv-formats | the option formats (see Formats) |
ajv.addKeyword('x-internal') (an annotation) | the option keywords: ['x-internal'] |
ajv.addKeyword() with validate, compile or macro | a definition in the option keywords, with the same three kinds (see Keywords of your own) |
ajv.addVocabulary([...]) | keywords: [...] |
ajv-keywords (require('ajv-keywords')(ajv)) | keywords: ajvKeywords(), with the same results, except transform, dynamicDefaults and select |
standaloneCode(ajv, validate) | standaloneJsonSchema(schema, options) (see Standalone code) |
ajv checks every schema against its meta-schema when compiling (validateSchema: true). schiva
checks keywords and types (an unknown keyword or { "type": 5 } throws), but not every value
against the meta-schema: { "minimum": "1" } compiles. To check schemas as ajv did, for example
in a test, use the meta-schema that ajv ships:
const metaSchema = require('ajv/dist/refs/json-schema-draft-07.json');
const validateSchema = compileJsonSchema({ $ref: 'http://json-schema.org/draft-07/schema#' }, { schemas: [metaSchema] });
validateSchema({ type: 5 }); // ['type must be one of: array, boolean, ...', ...]
Options
| ajv option | schiva |
|---|---|
allErrors: true | the default; allErrors: false for the first error only (ajv's default) |
strict, strictSchema | strict: true by default, an unknown keyword throws; strict: false ignores it. There is no 'log': nothing is logged. |
strictTypes | a keyword the type excludes ({ "type": "string", "minimum": 1 }) throws; with strict: false it is ignored, as the standard says |
strictTuples, strictRequired | no such checks; required keys that properties does not declare are checked as usual |
validateSchema, meta | see above: keywords and types are checked, values are not |
schemas, addUsedSchema | schemas, for each call |
validateFormats, ajv-formats | formats |
useDefaults, removeAdditional, coerceTypes | the same options, with the same results (see Changing the data and Differences) |
multipleOfPrecision | multipleOfPrecision, the same |
discriminator: true | always on, with ajv's rules and OpenAPI's mapping and implicit names too (see Discriminator) |
loadSchema | an option of compileJsonSchemaAsync() |
ownProperties | always: only own properties count (ajv reads inherited ones by default) |
unicode (default true) | always: string lengths count code points |
verbose, messages | errors always have their message; objects have params too |
code: { source: true }, code.esm | standaloneJsonSchema(), with format: 'esm' |
$data | not supported (see below) |
passContext | not needed: keyword functions receive (value, data, parentSchema) |
logger, uriResolver, inlineRefs, loopRequired, loopEnum, code.optimize, code.lines | not needed: schiva logs nothing, resolves URIs itself and chooses how to generate code |
int32range, parseDate, timestamp (JTD) | no JTD, so not needed |
Differences in results
On valid schemas, schiva and ajv agree on which values are valid, except in these cases. Most come from ajv and schiva reading the standard differently where ajv chose to depart from it; check the ones that apply to you.
| Case | ajv | schiva | What to do |
|---|---|---|---|
Schema without $schema | the draft of the class (Ajv2020: 2020-12) | draft-07 | pass draft |
format without the formats option | strict mode throws: unknown format | not checked, as the standard allows | pass formats: true (as you had ajv-formats) |
A keyword the type excludes | warns (strictTypes) | throws | fix the schema, or strict: false |
Inherited properties (Object.create(proto), class instances) | count as present | do not count | validate plain data, such as parsed JSON |
coerceTypes inside anyOf / oneOf | converts inside each subschema, even ones that then do not apply | converts by the schema's own type only | list the types in one type: ["number", "boolean"] |
default inside anyOf, oneOf, not, if with useDefaults | ignored | throws (ignored with strict: false) | move the default to a schema that always applies |
discriminator with mapping | rejected | supported, as in OpenAPI | nothing |
discriminator with unevaluatedProperties | wrong results: properties of the picked schema count as unevaluated | correct | nothing |
2019-09 / 2020-12: $dynamicRef, $recursiveRef, some unevaluated* cases | fails some tests of the suite | passes them | nothing, unless you relied on ajv's result |
Errors
By default a schiva function returns messages that start with the path of the field, ready for logs and HTTP
responses: 'lines[0].price must be at least 0'. For code that reads the errors, as with
validate.errors, use errors: 'objects':
const validate = compileJsonSchema(schema, { errors: 'objects' });
validate({ lines: [{ price: -1 }] });
// [{ path: ['lines', 0, 'price'], pointer: '/lines/0/price', keyword: 'minimum', params: { limit: 0 },
// message: 'lines[0].price must be at least 0' }]
| ajv error | schiva error object |
|---|---|
instancePath ('/lines/0/price') | pointer, the same JSON Pointer; and path, as an array (['lines', 0, 'price']) |
keyword | keyword, with the same names, and nullable for a null the type does not allow |
params | params: limit, allowedValues, allowedValue, pattern, format, type, multipleOf as in ajv; see the differences below |
message ('must be >= 0') | message, with the field in front: 'lines[0].price must be at least 0' (see Messages) |
schemaPath | not given |
required: at the object, with params.missingProperty | required: at the path of the missing property |
dependentRequired: at the object | at the path of the missing property, with params.property and params.missingProperty |
additionalProperties: at the object, with params.additionalProperty | at the path of the unexpected key, with params.property |
unevaluatedProperties: at the object, with params.unevaluatedProperty | at the path of the unexpected key, with params.property |
propertyNames: the error of the check, then one with params.propertyName, both at the object | only the error of the key's check, at the key's path, with propertyName: true |
Other differences, for code that reads the errors closely:
-
anyOf: both give the errors of each subschema; ajv adds one more, with the keywordanyOf.contains: ajv gives the errors of each element and onecontainserror; schiva gives only thecontainserror. -
oneOfmatching several schemas:params.passing(how many) instead ofparams.passingSchemas(which). -
minimum,maximumand the exclusive ones have noparams.comparison: the keyword says it.uniqueItemshas noparams.iandparams.j. -
A value that is not a number where an integer is expected gives
params.type: 'number'(must be a number); a number with decimals gives'integer'. ajv gives'integer'for both. -
nullwhere the type does not allow it has the keywordnullableand the messagecannot be null; ajv gives atypeerror.
Messages
If tests or code compare messages, they change. The messages of the most common keywords, for a property
a:
| Keyword | ajv (with instancePath /a) | schiva |
|---|---|---|
type | must be string | a must be a string |
required | must have required property 'a' (at the object) | a is mandatory |
additionalProperties | must NOT have additional properties (at the object) | Unexpected key: a |
minimum / maximum | must be >= 0 / must be <= 10 | a must be at least 0 / a must be at most 10 |
exclusiveMinimum | must be > 0 | a must be greater than 0 |
multipleOf | must be multiple of 5 | a must be a multiple of 5 |
minLength / maxLength | must NOT have fewer than 3 characters | a must be at least 3 characters long |
pattern | must match pattern "^[a-z]+$" | a does not match the required pattern |
format | must match format "email" | a must be a valid email |
enum | must be equal to one of the allowed values | a must be one of: x, y |
const | must be equal to constant | a must be equal to x |
minItems / maxItems | must NOT have fewer than 2 items | a must have at least 2 elements |
uniqueItems | must NOT have duplicate items (items ## 0 and 1 are identical) | a must not have duplicate elements |
minProperties | must NOT have fewer than 1 properties | a must have at least 1 properties |
dependentRequired | must have property b when property a is present | b is mandatory when a is present |
oneOf | must match exactly one schema in oneOf | a must match exactly one schema, but matches more than one |
not | must NOT be valid | a must not match the excluded schema |
propertyNames | property name must be valid | Key abc must be at most 2 characters long |
The value itself is called Value: Value must be a string. For messages of your own,
see Custom messages.
Formats
schiva checks the formats itself, without a plugin:
| ajv | schiva |
|---|---|
addFormats(ajv) | { formats: true } |
addFormats(ajv, ['date', 'email']) | { formats: ['date', 'email'] } |
ajv.addFormat('phone', /^\+\d+$/) | { formats: { phone: /^\+\d+$/ } } |
ajv.addFormat('even', { validate: (s) => ... }) | { formats: { even: (s) => ... } } |
ajv.addFormat('version', { validate, compare }) | { formats: { version: { validate, compare } } } |
formatMinimum, formatMaximum and the exclusive ones (ajv-formats) | the same keywords, with the same results (see Formats) |
addFormats(ajv, { mode: 'fast' }) | one mode: the checks follow the RFCs and pass every format test of the suite |
ajv.addFormat('int32', true) (known, not checked) | { formats: { ...builtInFormats(), int32: false } } |
| unknown format: throws in strict mode | the same: a format the option does not name throws, unless strict: false |
schiva also checks idn-hostname and idn-email. Following the RFCs, it can disagree
with ajv-formats on unusual values: it accepts an email with a quoted local part
("john doe"@example.com), which ajv rejects, and rejects 2026-02-30, which ajv's fast
mode accepts. Run your test data through both if formats matter to you.
Several documents
Documents that $ref points to go in the option schemas, by URI or with their
$id. Relative ids work as in ajv and Fastify: a document with "$id": "address" is
reached by "$ref": "address#". To load documents as ajv's compileAsync() does, use
compileJsonSchemaAsync() with the same loadSchema function: it is called once for
each document the schema reaches and schemas does not have.
compileJsonSchema(orderSchema, { schemas: [addressSchema, countrySchema] }); // schemas with "$id"
await compileJsonSchemaAsync(orderSchema, { loadSchema }); // loads them
A reference that does not resolve throws when compiling. Each document is read in the draft its own
$schema names, so a 2020-12 schema can reference draft-07 documents.
An ajv-compatible adapter
When much code reads validate.errors, this adapter gives it what it expects while the
validation runs on schiva. Copy it into your project, replace new Ajv(options) with
createAjvLike(options), and move the code to schiva's own API when convenient.
// ajv-like.js: ajv's compile(), validate.errors and error objects, on schiva.
const { compileJsonSchema } = require('schiva');
// Errors that ajv reports at the object, with the key in params.
const AT_OBJECT = {
required: 'missingProperty',
dependentRequired: 'missingProperty',
additionalProperties: 'additionalProperty',
unevaluatedProperties: 'unevaluatedProperty',
};
const COMPARISONS = { minimum: '>=', maximum: '<=', exclusiveMinimum: '>', exclusiveMaximum: '<' };
function toAjvError(error) {
const parent = error.pointer.slice(0, error.pointer.lastIndexOf('/'));
const key = error.path[error.path.length - 1];
const base = { schemaPath: '', keyword: error.keyword, message: error.message };
if (AT_OBJECT[error.keyword]) {
const params = error.keyword === 'dependentRequired' ? error.params : {};
return { ...base, instancePath: parent, params: { ...params, [AT_OBJECT[error.keyword]]: key } };
}
if (error.propertyName) {
return { ...base, instancePath: parent, keyword: 'propertyNames', params: { propertyName: key } };
}
const comparison = COMPARISONS[error.keyword];
return { ...base, instancePath: error.pointer, params: comparison ? { comparison, ...error.params } : error.params };
}
// Like new Ajv(options), with schiva's options (formats, schemas, draft...).
function createAjvLike(options = {}) {
const cache = new WeakMap();
// ajv stops at the first error unless allErrors: true.
const compileOptions = { ...options, allErrors: options.allErrors === true, errors: 'objects' };
return {
compile(schema) {
if (!cache.has(schema)) {
const check = compileJsonSchema(schema, compileOptions);
const validate = (data) => {
const errors = check(data);
validate.errors = errors.length === 0 ? null : errors.map(toAjvError);
return errors.length === 0;
};
cache.set(schema, validate);
}
return cache.get(schema);
},
errorsText(errors) {
return errors ? errors.map((error) => error.message).join(', ') : 'No errors';
},
};
}
module.exports = { createAjvLike };
instancePath, keyword and params then match ajv's for the common
keywords (schemaPath is empty); the messages are schiva's, and the remaining differences are
listed in Errors.
Comparing both
Before switching, validate your test data (or a sample of production data) with both libraries and look at the values where they disagree. It takes a few lines, for example in a test:
const Ajv2020 = require('ajv/dist/2020');
const addFormats = require('ajv-formats');
const { compileJsonSchema } = require('schiva');
const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
function compareOn(schema, values) {
const withAjv = ajv.compile(schema);
const withSchiva = compileJsonSchema(schema, { draft: '2020-12', formats: true, errors: false });
return values.filter((value) => withAjv(value) !== withSchiva(value));
}
// [] when both agree on every value.
console.log(compareOn(orderSchema, orderSamples));
With useDefaults, removeAdditional or coerceTypes, give each library its
own copy of each value (structuredClone(value)), as both change it.
Fastify
Fastify validates with ajv, with the options coerceTypes: 'array', useDefaults: true,
removeAdditional: true and allErrors: false. schiva has the same options, so a
validator compiler keeps the behavior of your routes:
const Fastify = require('fastify');
const { compileJsonSchema } = require('schiva');
// The schemas you gave to fastify.addSchema(), which a custom compiler does not see.
const shared = [{ $id: 'address', type: 'object', required: ['city'], properties: { city: { type: 'string' } } }];
const app = Fastify();
app.setValidatorCompiler(({ schema }) => {
const validate = compileJsonSchema(schema, {
schemas: shared,
// Fastify's defaults with ajv:
coerceTypes: 'array',
useDefaults: true,
removeAdditional: true,
allErrors: false,
});
return (data) => {
const errors = validate(data);
return errors.length === 0 ? { value: data } : { error: new Error(errors.join(', ')) };
};
});
app.post('/orders', {
schema: {
body: {
type: 'object',
required: ['qty', 'address'],
properties: { qty: { type: 'integer', minimum: 1 }, address: { $ref: 'address#' } },
},
},
}, async (req) => req.body);
// POST { "qty": 0, "address": {} } -> 400 { "message": "qty must be at least 1", ... }
Response schemas are serialized by fast-json-stringify, not validated, so they are not affected. Add
formats: true if your schemas use format, as Fastify includes ajv-formats.
OpenAPI
OpenAPI 3.1 schemas are JSON Schema 2020-12: pass draft: '2020-12'. OpenAPI 3.0 schemas are an
older dialect, which reads like draft-04 (exclusiveMinimum is a boolean) with a few more
keywords:
const validate = compileJsonSchema(openApi30Schema, {
draft: 'draft-04',
// OpenAPI's formats that are not checked, besides the built-in ones.
formats: { ...builtInFormats(), int32: false, int64: false, float: false, double: false, byte: false, binary: false, password: false },
// OpenAPI's annotations, and your extensions: or strict: false to ignore every unknown keyword.
keywords: ['example', 'xml', 'externalDocs', 'x-internal'],
});
nullable: trueis supported, as in ajv: it allowsnull.-
discriminatoris supported without an option, withmappingand implicit names, which ajv rejects. See Discriminator. -
readOnly,writeOnly,deprecated,titleanddescriptionare annotations and need nothing. -
References between components (
#/components/schemas/Pet): compile a schema that references the component, with the whole document inschemas:compileJsonSchema({ $ref: 'openapi.json#/components/schemas/Pet' }, { schemas: { 'openapi.json': document } }).
Custom messages
ajv-errors (errorMessage) and ajv-i18n change messages. With schiva, build them from the
keyword and params of error objects, in one place:
const validate = compileJsonSchema(schema, { errors: 'objects' });
const MESSAGES = {
required: () => 'es obligatorio',
minimum: (params) => `debe ser como mínimo ${params.limit}`,
format: (params) => `no es un ${params.format} válido`,
};
function messagesOf(errors) {
return errors.map((error) => {
const message = MESSAGES[error.keyword];
return message ? `${error.path.join('.')} ${message(error.params)}` : error.message;
});
}
messagesOf(validate({ age: 3 })); // ['name es obligatorio', 'age debe ser como mínimo 18']
For a message per field, as errorMessage gives, key the table by error.pointer
instead. Keep errorMessage out of the schemas (it throws as an unknown keyword), or declare it
with keywords: ['errorMessage'] to leave it as an annotation.
Standalone code
standaloneJsonSchema(schema, options) gives the source of a module to write to a file when
building, as ajv's standaloneCode() and ajv-cli do. There is no instance to create, the options
are those of compileJsonSchema() with format: 'commonjs' or 'esm', and
the module needs nothing else, not even schiva:
// build-validators.js, run when building (in place of ajv-cli compile)
const fs = require('fs');
const { standaloneJsonSchema } = require('schiva');
fs.writeFileSync('validate-order.mjs', standaloneJsonSchema(orderSchema, { formats: true, format: 'esm' }));
See Standalone code for several validators in one module.
TypeScript
| ajv | schiva |
|---|---|
ajv.compile<T>(schema): a type guard | compileJsonSchema<T>(schema, { errors: false }): a type guard for T |
JSONSchemaType<T>: checks a schema against a type | no counterpart; with the DSL, the type comes from the schema instead: type Order = Infer<typeof order> |
ErrorObject | ErrorObject (path, pointer, keyword, params, message) |
AnySchema, SchemaObject | JsonSchema, JsonSchemaObject |
Options | JsonSchemaOptions & CompileOptions |
What schiva does not do
| ajv feature | In schiva |
|---|---|
$data references | Not supported: $data as a keyword throws. Careful: inside the value of const or enum, { "$data": "1/b" } is not rejected but read as a plain value, as the JSON Schema standard says. Check those comparisons in your code. |
Keywords that write generated code (addKeyword with code) | Not supported: define them with validate, compile or macro. |
ajv-keywords transform, dynamicDefaults, select | Not supported: the first two change the data when validating, and select needs $data. Transform or fill the data before validating. |
Asynchronous validation ($async schemas) | $async throws (ignored with strict: false): validation is synchronous. Loading documents is supported, with compileJsonSchemaAsync(). Run asynchronous checks (a database lookup) after validating. |
JSON Type Definition (JTD, ajv/dist/jtd) | Not supported. |
ajv-errors (errorMessage), ajv-i18n | Build messages from error objects (see Custom messages). |
ajv-merge-patch ($merge, $patch) | Not supported: merge the schemas in your code before compiling, or use allOf. |
schemaPath in errors | Not given; errors have the path of the value, the keyword and its params. |
| Checking schemas against the meta-schema when compiling | Keywords and types are checked, values are not; compile the meta-schema to check them (see API). |
Checklist
- Run both libraries on your schemas and test data, and look at any disagreement.
- Replace
ajv.compile(schema)withcompileJsonSchema(schema, options), compiled once, when the program starts (or cached, see Replacing the instance). - If you used
Ajv2019orAjv2020with schemas without$schema, passdraft. - Move
addSchema()documents to the optionschemas, ajv-formats toformats: true, and ajv-keywords tokeywords: ajvKeywords(). - Pick the result: messages (default),
errors: 'objects'in place ofvalidate.errors, orerrors: falsefortrue/false. - If you relied on ajv stopping at the first error, add
allErrors: false. - Compile every schema once: an unknown keyword, or one the
typeexcludes, throws and shows what to change (or usestrict: false). - Look for
$data,$async,errorMessageandtransform, which need changes;useDefaults,removeAdditionalandcoerceTypesare the same options. - Where error objects are read, map
instancePathtopointer, andrequiredandadditionalPropertieserrors to their new paths, or use the adapter. - Update tests that compare messages (see Messages).
- Uninstall ajv and its plugins.