Skip to main content

Validators

Validators allow you to specify the contract of what constitutes valid data for the schema. They are called as the final check during CompiledSchema.process(), but are the only handler called during CompiledSchema.validate().

Validators are useful for adding additional checking beyond what is provided in the prebuilt fundamental schema types. For example, you might want a number property that only accepts positive numbers, or a string that must be a properly formatted URL.

Validators are expected to throw an error if their constraints are not met, or return a validated value.

import { Schema, SchemaResolver } from '@versionzero/schema';
const resolver = new SchemaResolver();

function evenConstraint(value) {
if (value % 2 !== 0) {
throw new Error(`${value} is not an even number`);
}
return value;
}

const schema = await resolver.compile(
new Schema('number').validator(evenConstraint)
);

console.log( await schema.validate(2) ); // -> 2
console.log( await schema.validate(3) ); // -> throws a ValidationError

As with any handler, validator processors may be asynchronous, allowing validation of not only the data, but what the data represents:

For example:

import { Schema, SchemaResolver } from '@versionzero/schema';
const resolver = new SchemaResolver();

resolver.registerValueProcessor('is-alive',
async server => {
if ((await fetch(`http://${server.host}:${server.port}/`)).ok) {
return server;
}
throw new Error('http server is not available');
}
)

const schema = await resolver.compile(
new Schema('object')
.property('server', new Schema('object')
.property('host', new Schema('string')
.default('localhost')
.validator('$hostname')
.validator('$reachable')
)
.property('port', new Schema('number')
.default(8080)
.validator('$port'))
.validator('$is-alive')
)
);

console.log(await schema.process({server: {port: 3000}}));

Validation Results

The validated output value does not necessarily need to be identical to the input value; validators can also perform trivial processing for consistency, producing a "more valid" result, as long as it still meets the contract. For example, trimming strings, or standardizing the case of an identifier. However, they should be idempotent, and return the same value on a second validation. See revalidation below.

import { Schema, SchemaResolver } from '@versionzero/schema';
const resolver = new SchemaResolver();

// $email by default will return the email in lowercase
const schema = await resolver.compile(
new Schema('string').validator('$email')
)
console.log( await schema.process( 'SomeUser@DOMAIN.TLD') ); // -> someuser@domain.tld
console.log( await schema.process( 'Funky@@DOMAIN.TLD') ); // -> throws ValidationError

Revalidation

If a validator returns a different value than its input, it will be revalidated by default, to ensure that the updated value is also valid.

This is useful for catching coding errors, most commonly where the validator incorrectly returns the result of a boolean comparison:

// Will throw a validation error; upon rerunning the validator pipeline,
// the string validator rejects the new boolean result:
new Schema('string')
.validator(v => (v.length > 0)); // incorrect validator!

If your validation pipeline both updates the output and contains expensive operations, this safety net might not be desirable. For this reason, you can disable the schema's revalidate option:

new Schema('string')
.option('revalidate', false)
.validator('$lowercase')
.validator('$hostname')
.validator('$reachable')