Skip to main content

Schema Inheritance

If you pass another schema as the first parameter to the Schema constructor, the newly constructed schema instance will inherit its behavior:

const myStringSchema = new Schema().normalizer(value => String(value));
const myRequiredStringSchema = new Schema(myStringSchema).required();
const myRequiredNonEmptyStringSchema = new Schema(myRequiredStringSchema)
.validator(v => { if (v.length) { return v } else { throw new Error('string cannot be empty!') } })

Built-in Base Schemas and Validators provided by SchemaResolver

Although you could manage your own library of reusable schemas and validators, the SchemaResolver class provides a schema registry that is preloaded with Schema instances for handling fundamental JS types, including string, number, boolean, object, array, date, and buffer.

As an alternative to passing an actual Schema instance as the first parameter to the Schema constructor, you can instead pass the registered name of any schema stored in the SchemaResolver registry.

SchemaResolver also provides a registry of 30+ predefined named validators functions such as $email, $integer, and $alphanum, as well as validator operators like $and, $or, and $range. These can be provided as the validator option instead of passing a function. See the validator section for the full list. // TODO - validators documented in a guide! link here!

Here is an example that combines both features:

const requiredEmailSchema = new Schema('string')
.required()
.validator('$email')

You can also use the SchemaResolver to register your own custom schemas for reuse:

const resolver = new SchemaResolver();

const locationSchema = new Schema('object')
.property('location', new Schema('array')
.property('0', new Schema('number')
.required()
.validator({$range: {$min: -90, $max: 90}})
.meta('valueName', 'latitude')
)
.property('1', new Schema('number')
.required()
.validator({$range: {$min: -180, $max: 180}})
.meta('valueName', 'longitude')
)
)
resolver.registerSchema('location', locationSchema);

// elsewhere in the code...

const tripSchema = new Schema('object')
.property('origin', new Schema('location').meta('description', 'start point'))
.property('destination', new Schema('location').meta('description', 'end point'))

Note that references to a base schema or validator in a Schema are not immediately resolved.

SchemaResolver and CompiledSchema

The Schema class is what you use to build your schema, but it isn't directly usable for configuration or validation. In order to resolve referenced base classes and validators, the schema needs to be compiled by the SchemaResolver into a CompiledSchema:

import { Schema, SchemaResolver, CompiledSchema } from '@versionzero/configurator';

const resolver = new SchemaResolver();

// the base schema and validator are only references in this schema...
const schema = new Schema('string')
.required()
.validator({$and: ['$email', /@thiscorp\.com$/]})

// ...but they are resolved during compilation!
const compiledSchema = resolver.compile(schema);

for (const email of ['john@thiscorp.com', 'fred@thatcorp.com', 'potato']) {
try {
await compiledSchema.validate(email);
console.log(`"${email}" is valid`);
}
catch (error) {
console.error(`"${email}" is invalid: ${error.message} (${error.cause?.message})`);
}
}

Running this code produces:

"john@thiscorp.com" is valid
"fred@thatcorp.com" is invalid: Validation error (Value does not match pattern /@thiscorp\.com$/)
"potato" is invalid: Validation error (Invalid email format)

(You don't usually need to manually compile your schema, as compilation is performed automatically by Configurator during the configure call.)

Compilation results in a CompiledSchema hierarchy, with all properties also compiled to CompiledSchema instances. For each schema, the inheritance chain is resolved and flattened, options are processed and checked for errors, and all handler options are resolved and wrapped in async functions.

The CompiledSchema provides the active value processing capabilities, and is the form used by configuration sources to discover what can be configured, and is also passed to schema value handler options.