Guide

schiva checks that JavaScript values have the shape you expect. You describe that shape with a schema, written in JavaScript with its own syntax or in JSON Schema (any version from draft-04 to 2020-12). schiva compiles the schema once into a function, and that function returns the list of errors of each value you give it: an empty list when the value is valid.

Getting started

npm install schiva

schiva has no dependencies and runs on Node.js 18 and later, and in browsers through any bundler. It exports the same names with require and with import.

const { ClosedSchema, String, Integer, ArrayOf } = require('schiva');

const person = new ClosedSchema({
  id: String(),
  age: Integer({ min: 18 }),
  tags: ArrayOf({ type: String(), isMandatory: false }),
});

// Compile once, when the program starts:
const validatePerson = person.compile();

validatePerson({ id: 'x', age: 20 }); // []
validatePerson({ id: 1, age: 10, extra: 1 });
// ['id must be a string', 'age must be at least 18', 'Unexpected key: extra']

Modes

compile(options) (on schemas and on every type) and compileJsonSchema(json, options) return a function. The options choose what it returns:

OptionThe function returnsUse it when
(none)every error message, [] when validmessages are shown or logged
{ allErrors: false }only the first error message, [] when validone message is enough; stops at the first failing check
{ errors: false }true or falseonly validity matters; builds no messages
{ errors: 'objects' }every error as an object, [] when valid (with allErrors: false, the first)errors are handled by code: forms, translations; see Error objects
const isPerson = person.compile({ errors: false });

if (!isPerson(body)) {
  // reject the request
}

Schemas and types also have validate(value), which checks without compiling (10 to 25 times slower), and isValid(value).

Compile once

The compiled function is a snapshot of the schema: changes made to the schema or its types afterwards (such as .optional(), .nullable() or setting their fields) are not seen. Compile after building the schema, and compile again if it changes.

Compiling takes about 0.1 ms for a schema of moderate size, so compile when the program starts (or cache the function), not for every value.

// Compiled once, reused for every request.
const validateOrder = orderSchema.compile();

app.post('/orders', (req, res) => {
  const errors = validateOrder(req.body);
  if (errors.length) return res.status(400).json({ errors });
  // ...
});

compile() creates the function with new Function, which does not run where code generation is forbidden (for example with a strict Content Security Policy). There, generate standalone code when building.

Standalone code

standaloneCode(type, options) returns the code of a validator as the source of a JavaScript module, to save to a file when building and load like any other. Loading it generates no code, so it runs under a strict Content Security Policy (without 'unsafe-eval') and in runtimes that forbid code generation, and it needs nothing else: not even schiva, as the few helpers it calls are written into it.

// build-validators.js, run when building
const fs = require('fs');
const { standaloneCode, standaloneModule, standaloneJsonSchema } = require('schiva');

fs.writeFileSync('validate-person.js', standaloneCode(person)); // module.exports = the validation function
fs.writeFileSync('validators.mjs', standaloneModule({ isPerson: person, isOrder: order }, { errors: false, format: 'esm' }));
fs.writeFileSync('validate-address.js', standaloneJsonSchema(addressSchema, { schemas: [countrySchema] }));
// In the application
const validatePerson = require('./validate-person.js');
import { isPerson } from './validators.mjs';

The options are those of compile() (allErrors, errors) and format: 'commonjs' (the default) or 'esm'. standaloneCode() exports one function (module.exports, or the default export), and standaloneModule() one for each name. standaloneJsonSchema() takes the options of compileJsonSchema() too. The functions return the same results as the ones compile() gives: schiva checks it on every test of the JSON-Schema-Test-Suite (pnpm run conformance draft2020-12 --standalone). Types of your own cannot be written out, as their validate() runs when validating, and throw.

DSL or JSON Schema

Both compile to the same types and the same generated code, so the speed is the same.

Schema DSL

A Schema takes an object whose keys are the expected properties. Plain objects inside it become nested schemas.

const { Schema, String, Float, Enum, ArrayOf, Integer } = require('schiva');

const order = new Schema(
  {
    id: String({ pattern: /^ORD-\d{8}$/ }),
    status: Enum({ options: ['draft', 'placed'] }),
    customer: { name: String({ min: 1 }), email: String({ isMandatory: false }) },
    lines: ArrayOf({ type: { sku: String(), qty: Integer({ min: 1 }) }, min: 1 }),
    total: Float({ min: 0 }),
  },
  { isOpen: false }
);

Wherever a type is expected (ArrayOf, AnyOf, Not, additionalType...), a plain object stands for new Schema(object), as lines shows above. That schema is open even inside a ClosedSchema; write new ClosedSchema({ ... }) to reject unknown keys there too. A value that is neither a type nor an object of types throws when the type is built.

Types

Every type takes isMandatory (default true: undefined is an error) and isNullable (default false: null is an error), and has .optional(), .required(), .nullable() and .notNull().

TypeOptions
Stringmin, max (length), pattern (RegExp), format (a built-in format), allowEmpty, countCodePoints
Integer, Floatmin, max, exclusiveMin, exclusiveMax, multipleOf
Boolean, Any, Never
Enumoptions (strings)
Values, Const(value)values: allowed values, compared deeply
ArrayOftype (one type, or an array of types for a tuple), min, max, unique, contains, additionalType
AnyOf, AllOf, OneOftypes
Nottype
ConditionalifType, thenType, elseType
WhenjsonType (object, array, string, number), type: checks only values of that JSON type
Reftarget, for recursive schemas

Schema options

OptionMeaning
isOpendefault true; false rejects unknown keys. ClosedSchema sets it to false.
additionalTypetype of the keys that are not declared
patternTypes[{ pattern, type }]: type of the keys that match a pattern
propertyNameTypetype that every key must match
minProperties, maxPropertiesnumber of keys
dependencies[{ key, required: [...] }] or [{ key, type }]
isMandatory, isNullableas for every type

Declared keys are read as own properties only, so {}.toString or a key added to Object.prototype never counts as present.

Short helpers

str, int, float, bool, arrOf, obj, any, anyOf, allOf, oneOf, not, enumt, and optional versions prefixed with o (ostr, oint...). arrOf(String()) and arrOf({ sku: String() }) take the type of the elements; arrOf({ type, min, ... }) takes the options.

Recursive schemas

Create the Ref first and point it at the schema once the schema exists:

const { Schema, Integer, ArrayOf, Ref } = require('schiva');

const child = Ref();
const node = new Schema({ value: Integer(), children: ArrayOf({ type: child, isMandatory: false }) });
child.target = node;

node.compile()({ value: 1, children: [{ value: 2, children: [{ value: 'x' }] }] });
// ['children[0].children[0].value must be a number']

JSON Schema

fromJsonSchema(json, options) converts a JSON Schema into the same types, and compileJsonSchema(json, options) compiles it. Every validation keyword of draft-07 is supported, along with definitions (or $defs) and $ref (JSON pointers, $id base URIs and anchors, recursive schemas). String lengths count Unicode code points, as the specification says. multipleOf divides in floating point, so 0.3 is not a multiple of 0.1; with the option multipleOfPrecision: 8, a division within 1e-8 of an integer passes, as with ajv's option. Drafts 04, 06, 2019-09 and 2020-12 are supported completely: see Drafts.

Unknown keywords

A keyword schiva does not know throws when compiling, with the place where it was found:

compileJsonSchema({ type: 'string', maxLenght: 10 });
// Error: Unsupported JSON Schema keyword "maxLenght" at #

Annotations (title, description, default, examples, $comment, readOnly, writeOnly, deprecated) are accepted and ignored.

The content keywords (contentMediaType, contentEncoding, contentSchema) are annotations too.

Keywords of your own, such as the x- extensions of OpenAPI, can be declared as annotations with the option keywords, and typos are still caught. strict: false ignores every unknown keyword instead, and also the ones of other drafts and the ones a type excludes, as the JSON Schema standard reads a schema:

compileJsonSchema(schema, { keywords: ['x-internal', 'example'] });
compileJsonSchema(schema, { strict: false }); // like ajv's strict: false

Keywords of your own

The option keywords also takes definitions of keywords that check values, like ajv's addKeyword(). A definition has its keyword, optionally the JSON type of the values it checks (every value by default), and one function:

message gives the text of its error after the name of the value, or a function (value, data) => text does (must pass the "<keyword>" keyword by default). Its error objects have its name as keyword.

const even = { keyword: 'even', type: 'integer', validate: (value, data) => !value || data % 2 === 0, message: 'must be even' };
const between = { keyword: 'between', type: 'number', macro: ([min, max]) => ({ minimum: min, maximum: max }) };

const validate = compileJsonSchema(
  { properties: { seats: { type: 'integer', even: true, between: [2, 8] } } },
  { keywords: [even, between] }
);

validate({ seats: 3 }); // ['seats must be even']
validate({ seats: 10 }); // ['seats must be at most 8']

ajvKeywords() gives the keywords of ajv-keywords, with the same results: typeof, instanceof, range, exclusiveRange, regexp, uniqueItemProperties, allRequired, anyRequired, oneRequired, patternRequired, prohibited, deepProperties and deepRequired. Name the ones you use, or leave the list out for all of them:

compileJsonSchema(schema, { keywords: ajvKeywords(['range', 'regexp']) });
compileJsonSchema(schema, { keywords: ['x-internal', ...ajvKeywords()] });

transform changes the data and dynamicDefaults computes defaults when validating (schiva only assigns fixed defaults, see useDefaults), and select needs $data references, so they are left out. Macros and regular expressions can be written into standalone code, functions cannot: among ajv-keywords, range, exclusiveRange, regexp (without the flags g and y), allRequired, anyRequired, oneRequired, prohibited and deepProperties can.

Changing the data

schiva only checks values unless you ask for one of these options, which change the value being validated as ajv's do:

const validate = compileJsonSchema(
  {
    type: 'object',
    properties: { name: { type: 'string' }, role: { type: 'string', default: 'user' } },
    required: ['name'],
    additionalProperties: false,
  },
  { useDefaults: true, removeAdditional: true }
);

const user = { name: 'Ann', password: 'secret' };
validate(user); // []
user; // { name: 'Ann', role: 'user' }

A value is converted by its schema's own type (or else by the first part of its allOf, or the schema its $ref points to), once; ajv also converts it inside anyOf and oneOf schemas, even ones that then do not apply. List the types in one type instead: type: ['number', 'boolean']. ajv only takes the element of an array of one for schemas with one type.

As in ajv, defaults inside anyOf, oneOf, not and if (directly or through $ref) are not assigned, as those schemas may not apply: they throw, or are ignored with strict: false. required, minProperties and maxProperties see the additional properties before they are removed. Removing properties inside anyOf, oneOf, not or if can remove them while trying a schema that then does not apply, and the result depends on the order the keywords run, which is not always ajv's: remove them in the schemas that always apply. An invalid value may be changed only in part, more so when stopping at the first error.

Discriminator

The discriminator of OpenAPI picks the oneOf schema that applies by the value of a property, which must be required. An object is only checked against the schema its value picks, with the errors of that schema, which is several times faster than trying every schema; a value that picks none gets an error about the property. Other values are checked as by oneOf. The value of each schema comes from, in this order:

const validate = compileJsonSchema({
  $defs: {
    Cat: { type: 'object', properties: { lives: { type: 'integer' } }, required: ['petType', 'lives'] },
    Dog: { type: 'object', properties: { bark: { type: 'boolean' } }, required: ['petType'] },
  },
  oneOf: [{ $ref: '#/$defs/Cat' }, { $ref: '#/$defs/Dog' }],
  discriminator: { propertyName: 'petType', mapping: { cat: '#/$defs/Cat', dog: 'Dog' } },
});

validate({ petType: 'cat' }); // ['lives is mandatory']
validate({ petType: 'cow' }); // ['petType must be one of: cat, dog']

With values from const and enum alone, no other schema accepts the value, so the result is the one of oneOf. With mapping or names, the property picks the schema as OpenAPI means it, whatever the other schemas accept. ajv only reads const and enum, and rejects mapping.

A oneOf without discriminator whose schemas give one property distinct string values with const or enum is checked the same way: the result is the one of oneOf, the errors of an object whose value picks a schema are the ones of that schema, and a value that picks none gets the errors of oneOf.

Formats

format is an annotation by default, as the drafts allow: nothing is checked. The option formats turns the checks on:

const validate = compileJsonSchema(
  { type: 'object', properties: { email: { type: 'string', format: 'email' }, since: { format: 'date' } } },
  { formats: true }
);

validate({ email: 'x', since: '2026-02-30' }); // ['email must be a valid email', 'since must be a valid date']

With the option, a format it does not name throws when compiling, as in ajv's strict mode, since it is most likely a typo ("emial"): name it, with false to leave it unchecked, or use strict: false to ignore unknown formats, as the standard reads them. Without the option, any format is an annotation. A format applies to strings only. The built-in formats follow their RFCs: dates and times with leap years and leap seconds (RFC 3339), email addresses with quoted local parts and IP literals, and internationalized host names with punycode, IDNA2008 and the Bidi rule. schiva passes every test of the optional format tests of the JSON-Schema-Test-Suite (793 of draft-07, 874 of 2019-09 and of 2020-12; pnpm run conformance draft2020-12 --formats); ajv with ajv-formats passes 655 and 733.

formatMinimum, formatMaximum, formatExclusiveMinimum and formatExclusiveMaximum limit the values of a format that can be compared (date, time and date-time), as ajv-formats does, with the same results:

const validate = compileJsonSchema(
  { type: 'string', format: 'date', formatMinimum: '2020-01-01', formatExclusiveMaximum: '2021-01-01' },
  { formats: true }
);

validate('2019-12-31'); // ['Value must be at least 2020-01-01']

Times and date-times compare by the moment they name, whatever their time zone. As in ajv the limits need format, and are checked only when the format is. A format of your own can be compared too, given as { validate, compare } in formats, where compare(a, b) gives a negative number, 0 or a positive number. Where ajv ignores a limit that is not a valid value of its format, schiva throws.

In the DSL, a String takes a built-in format too: String({ format: 'email' }).

Drafts

The draft comes from options.draft ('draft-04', 'draft-06', 'draft-07', '2019-09' or '2020-12'), or else from the $schema of the schema. Without either, or with another $schema, the schema is read as draft-07. Each schema resource inside it, or document it references, is read in the draft its own $schema names, so a 2020-12 schema can reference draft-07 ones. A $schema can also name a meta-schema given in options.schemas: its draft applies, and the keywords of the vocabularies its $vocabulary leaves out are ignored.

const validate = compileJsonSchema({
  $schema: 'https://json-schema.org/draft/2020-12/schema',
  prefixItems: [{ type: 'string' }, { type: 'integer' }],
  items: false,
});

validate(['a', 1]); // []
validate(['a', 1, 2]); // ['Value[2] is not allowed']

The older drafts differ from draft-07 in a few keywords. Draft-06 has no if/then/else. Draft-04 has no const, contains or propertyNames either, changes the base URI with id instead of $id, and makes exclusiveMinimum and exclusiveMaximum booleans that make minimum and maximum exclusive.

Drafts 2019-09 and 2020-12 add, and schiva supports:

const validate = compileJsonSchema({
  $schema: 'https://json-schema.org/draft/2020-12/schema',
  $defs: { base: { properties: { id: { type: 'integer' } } } },
  $ref: '#/$defs/base',
  properties: { name: { type: 'string' } },
  unevaluatedProperties: false,
});

validate({ id: 1, name: 'a' }); // []
validate({ id: 1, name: 'a', extra: true }); // ['Unexpected key: extra']

When the keys those keywords evaluate are the same for every value, as with $ref, allOf and properties, the check compiles to a loop over the other keys, as fast as additionalProperties. With anyOf, oneOf, if or dependentSchemas it depends on the value, and the generated code works it out first.

Dynamic references

$dynamicRef (2020-12) and $recursiveRef (2019-09) let a schema extend another that refers to itself: the reference points to the outermost schema being evaluated that has the same $dynamicAnchor (or $recursiveAnchor). Here a generic tree accepts any key, and the strict tree that extends it rejects unknown keys at every level:

const tree = {
  $schema: 'https://json-schema.org/draft/2020-12/schema',
  $id: 'https://example.com/tree',
  $dynamicAnchor: 'node',
  type: 'object',
  properties: { data: true, children: { type: 'array', items: { $dynamicRef: '#node' } } },
};
const validate = compileJsonSchema(
  {
    $schema: 'https://json-schema.org/draft/2020-12/schema',
    $id: 'https://example.com/strict-tree',
    $dynamicAnchor: 'node',
    $ref: 'tree',
    unevaluatedProperties: false,
  },
  { schemas: [tree] }
);

validate({ children: [{ daat: 1 }] }); // ['Unexpected key: children[0].daat']

The target of a dynamic reference depends on the schemas being evaluated, and schiva still works it out when compiling: a schema reached in different ways is compiled once for each, so the generated code stays as fast as for $ref.

Complete: schiva passes the whole JSON-Schema-Test-Suite of draft-04 (618 of 618), draft-06 (841 of 841), draft-07 (929 of 929), 2019-09 (1261 of 1261) and 2020-12 (1301 of 1301). pnpm run conformance draft2020-12 shows it file by file.

Several documents

References to other documents resolve against the documents given in options.schemas, as { uri: schema } or as an array of schemas with $id. compileJsonSchema() loads nothing, and a reference that cannot be resolved throws when compiling:

const validateOrder = compileJsonSchema(orderSchema, {
  schemas: { 'https://example.com/schemas/address.json': addressSchema },
});

URIs can also be relative, as ajv and Fastify allow: a document with "$id": "address" is reached by "$ref": "address#" from a schema without $id (or with a relative one).

const address = { $id: 'address', type: 'object', required: ['city'], properties: { city: { type: 'string' } } };
compileJsonSchema({ properties: { home: { $ref: 'address#' } } }, { schemas: [address] });

To load them instead, compileJsonSchemaAsync(json, options) asks options.loadSchema(uri), an async function you write, for each document the schema references and schemas does not have, and for the ones those reference in turn, once each (like ajv's compileAsync()). loadJsonSchemas(json, options) gives the documents it loaded, with options.schemas, to compile later or to write standalone code:

const validateOrder = await compileJsonSchemaAsync(orderSchema, {
  loadSchema: async (uri) => (await fetch(uri)).json(),
});

To validate schemas against the draft-07 meta-schema, register it in schemas (ajv ships a copy as ajv/dist/refs/json-schema-draft-07.json).

Errors

Errors are plain strings that start with the path of the field, so they can be logged or returned as they are:

[
  'customer.name is mandatory',
  'lines[1].sku must be a string',
  'lines[1].price must be at least 0',
  'Unexpected key: extra',
]

A value checked on its own (not inside a schema) is called Value: Integer().compile()('x') returns ['Value must be a number']. Keys are named the same way whether or not the schema combines keywords (allOf, $ref with other keywords, unevaluatedProperties...): name, not Value.name.

With every error (the default mode), the errors of every part of a schema are reported, including every schema of an allOf, and each message appears once even when two parts find the same problem.

Error objects

With errors: 'objects', the compiled function returns each error as an object, for code that handles them (showing them next to form fields, translating them, or moving from ajv):

order.compile({ errors: 'objects' })({ id: 1, lines: [{ price: -1 }] });
// [
//   { path: ['id'], pointer: '/id', keyword: 'type', params: { type: 'string' }, message: 'id must be a string' },
//   { path: ['lines', 0, 'price'], pointer: '/lines/0/price', keyword: 'minimum', params: { limit: 0 },
//     message: 'lines[0].price must be at least 0' },
// ]

It works with compileJsonSchema() and in standalone code too; validate() still gives messages.

TypeScript

schiva includes its type declarations. Infer gives the TypeScript type of the values a DSL schema accepts, and a function compiled with { errors: false } is a type guard:

import { Schema, String, Integer, ArrayOf, Enum, Infer } from 'schiva';

const person = new Schema({
  id: String(),
  age: Integer({ min: 18 }),
  status: Enum({ options: ['active', 'blocked'] }),
  tags: ArrayOf({ type: String(), isMandatory: false }),
  address: { city: String(), zip: String({ isNullable: true }) },
});

type Person = Infer<typeof person>;
// { id: string; age: number; status: 'active' | 'blocked'; address: { city: string; zip: string | null };
//   tags?: string[] | undefined }

const isPerson = person.compile({ errors: false });
if (isPerson(body)) {
  body.address.city; // body is a Person here
}

A key whose type accepts undefined (isMandatory: false, .optional()) is an optional property, and isNullable adds null. ArrayOf gives arrays and tuples, AnyOf and OneOf unions, and AllOf intersections. A recursive schema needs the type of its Ref: const child = Ref<TreeNode>().

The values a JSON Schema accepts are unknown to TypeScript; give their type to get a type guard: compileJsonSchema<User>(schema, { errors: false }).

Schemas from samples

inferJsonSchema(samples, options) writes the JSON Schema of a list of sample values, and inferSchemaCode(samples, options) the same schema in the DSL, as the source of a module. Try it in the browser with Schema from JSON. The samples are merged:

const { inferJsonSchema, inferSchemaCode } = require('schiva');

const samples = [
  { id: 1, name: 'Ann', email: 'ann@example.com', address: { city: 'Madrid' } },
  { id: 2, name: 'Bob', email: 'bob@example.com', address: null, phone: '+34 600 000 000' },
];

inferJsonSchema(samples);
// { $schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object',
//   properties: { id: { type: 'integer' }, name: { type: 'string' }, email: { type: 'string', format: 'email' },
//     address: { type: ['object', 'null'], properties: { city: { type: 'string' } }, required: ['city'] },
//     phone: { type: 'string' } },
//   required: ['id', 'name', 'email', 'address'] }

console.log(inferSchemaCode(samples));
// const { Integer, Schema, String } = require('schiva');
//
// const schema = new Schema({
//   id: Integer(),
//   name: String(),
//   email: String({ format: 'email' }),
//   address: new Schema({
//     city: String(),
//   }, { isNullable: true }),
//   phone: String({ isMandatory: false }),
// });

The options are closed (reject keys the samples do not have: additionalProperties: false, ClosedSchema), formats (false detects none) and draft (the $schema written, '2020-12' by default); inferSchemaCode() also takes name (of the variable, 'schema') and module ('commonjs', 'esm' or 'none'). A single value goes in a list: inferJsonSchema([value]). The schema accepts every sample and is a starting point: add the limits the data needs, such as lengths, ranges, patterns and enums.

Types of your own

Classes that extend ValidateType (or a built-in type) with their own validate() or isValid() keep working when compiled: the generated code calls them for their part of the schema. validate() returns undefined when the value is valid and an error message otherwise.

const { Schema, ValidateType } = require('schiva');

class Even extends ValidateType {
  validate(value, fieldName = 'Value') {
    const presence = super.validate(value, fieldName); // isMandatory and isNullable
    if (presence !== undefined || value === undefined || value === null) return presence;
    return value % 2 === 0 ? undefined : `${fieldName} must be even`;
  }
}

new Schema({ n: new Even() }).compile()({ n: 3 }); // ['n must be even']

API

The main functions; the API reference lists every function, type, option and error message.

Function or methodReturns
new Schema(shape, options), new ClosedSchema(shape, options)a schema for objects
type.compile(options)a validation function; see Modes
type.validate(value)the errors, without compiling
type.isValid(value)true or false, without compiling
compileJsonSchema(json, options)a validation function for a JSON Schema; takes the options of compile, and schemas, draft, formats, strict, keywords, useDefaults, removeAdditional and coerceTypes
ajvKeywords(names)definitions of the keywords of ajv-keywords, for the option keywords (see Keywords of your own)
compileJsonSchemaAsync(json, options)a promise of the same, loading the documents it references with options.loadSchema (see Several documents)
loadJsonSchemas(json, options)a promise of the documents a schema references, loaded with options.loadSchema
fromJsonSchema(json, options)the schiva type of a JSON Schema

Security

Report security problems privately through GitHub security advisories, not in public issues.

Performance

Faster than ajv in every benchmark we run. Compared with ajv and the other validators of json-schema-benchmark, each measurement in its own process. Higher is better; each library is compared in the same mode (first error or all errors). The benchmarks page has every library, every payload, the tests each one passes, and the speed of the features (keywords of your own, defaults, coercion, formats, error objects, standalone code).

JSON-Schema-Test-Suite

Runs per second over the test groups every validator passes. For drafts 2019-09 and 2020-12 only the validators that implement them run (ajv with its Ajv2019 and Ajv2020 classes).

draft-072019-092020-12
schiva, first error124k22.1k25.7k
ajv, first error60k4k–15k ¹5k–15k ¹
@exodus/schemasafe, first error98k14.9k17.1k
schiva, all errors101k17.1k19.4k
ajv, all errors51k3.6k–12k ¹5k–12k ¹
@exodus/schemasafe, all errors64k6.9k9.7k
schiva, true/false177k30.9k34.7k
@exodus/schemasafe, true/false147k24.0k27.5k
Tests passed: schiva / ajv / schemasafe929 / 921 / 9051261 / 1233 / 12281301 / 1239 / 1263

¹ ajv's speed on these two suites changes from one process to the next, between the two numbers given. schiva passes every test of the three suites. Of the other validators, the one that passes the most tests of drafts 2019-09 and 2020-12 is json-schema-library (1260 and 1291), at about 1/400 of the speed of schiva.

Payloads

Validations per second (compiling: schemas per second).

schiva, first errorajv, first errorschiva, all errorsajv, all errors
moltar strict, valid33.1M29.1M30.6M27.9M
moltar strict, invalid78.9M36.0M20.8M17.5M
Order with 20 lines, valid2.16M0.74M2.23M0.71M
Order with 20 lines, invalid17.1M12.2M1.86M0.44M
Order 2020-12 ($ref, allOf, unevaluatedProperties), valid2.32M0.66M2.29M0.65M
Order 2020-12, invalid31.7M15.0M1.53M0.38M
Payment 2020-12 (oneOf and unevaluatedProperties), valid15.2M8.8M15.3M8.0M
Payment 2020-12, invalid14.5M7.5M13.8M6.2M
Shapes, discriminator with 8 kinds, valid117M44.8M105M44.7M
Shapes, invalid122M37.1M44.3M19.7M
Compiling the order schema8.4k0.19k8.3k0.23k
pnpm run bench         # every validator in its own process (about 15 minutes)
pnpm run bench:quick   # every validator in one process (about a minute)
pnpm run conformance   # tests passed by schiva and ajv, file by file, for one draft

The method, and the payloads measured, are described in bench/. Benchmarks depend on the machine: run them with nothing else busy, as a validator measured while the computer is loaded looks several times slower than it is.

FAQ

Why are errors strings by default?

Most errors end up in a log or an HTTP response. Strings with the path in front are ready for that, and building them is cheap. For errors handled by code, use { errors: 'objects' } (see Error objects); to know only whether a value is valid, { errors: false }.

How do I move from ajv?

Your JSON Schemas stay as they are; the code around them changes a little. See Migrating from ajv.

Which drafts does it support?

Draft-04, draft-06, draft-07, 2019-09 and 2020-12, completely: schiva passes their whole JSON-Schema-Test-Suite, dynamic references and vocabularies included. See Drafts.

Does it work with OpenAPI schemas?

Yes: nullable and discriminator are supported, and the x- keywords can be declared with the option keywords (see Unknown keywords and Discriminator).

Does it check format?

When you ask it to, with the option formats (or String({ format }) in the DSL): see Formats.

Does it include TypeScript types?

Yes, and it infers the type of the values a DSL schema accepts: see TypeScript.