Extending the Library: Custom Rules
The real power of the library comes from its extensibility. You can easily add your own reusable types and validators. They must stay synchronous so the schema remains portable across environments. When you do this, you'll be passed a powerful context object.
The context Object
Every custom type and validator handler receives a context object as its only argument. This object is your toolbox, giving you all the information you need to perform complex logic. Here are its properties:
value: The current value of the field being processed. Be aware that this value may have already been changed by the type handler or a previous validator.fieldName: A string containing the name of the field currently being validated (e.g.,'username').object: The entire object that is being validated. Its properties reflect the data after any casting or transformations have been applied up to this point. This is useful for cross-field validation.valueBeforeCast: The original, raw value for the field, exactly as it was in the input object before any type casting occurred.objectBeforeCast: The original, raw input object, before any modifications were made.definition: The schema definition object for the current field. For a field defined as{ type: 'string', min: 5 }, this would be that exact object.parameterName: (For validators only) The name of the validation rule currently being executed (e.g.,'min').parameterValue: (For validators only) The value of the validation rule currently being executed (e.g., the5inmin: 5).mode: The active validation contract. Preserved as a compatibility alias foroperation.operation: The active validation contract name (for example'create','patch', or a custom operation such as'upsert').fieldPresent: A boolean indicating whether the field was explicitly present in the original input object.throwTypeError(): A function you can call to throw a standardizedTYPE_CAST_FAILEDerror. This is the preferred way to report an error from within a type handler.throwParamError(code, message, params): A function you can call to throw a standardized validation error from within a validator. It accepts a custom errorcode, amessage, and an optionalparamsobject.
Creating a Custom Validator
Let's say you frequently need to validate that a field is a URL-friendly "slug" (e.g., my-blog-post).
You can define a new validator once and use it anywhere.
Custom validators must be synchronous and local. If you need to ask a database or external API something, validate the payload first and run that business rule afterward in your service layer.
// Do this once when your application starts
createSchema.addValidator('slug', (context) => {
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
if (typeof context.value !== 'string' || !slugRegex.test(context.value)) {
// Use the public context method to throw a standardized error
context.throwParamError(
'INVALID_SLUG', // Custom error code
'Value must be a valid slug (e.g., my-post).'
);
}
});
// Now you can use 'slug' in any schema!
const articleSchema = createSchema({
title: { type: 'string', required: true },
slug: { type: 'string', required: true, slug: true } // Use it here
});Creating a Custom Type
A Type is used for casting. Imagine you want a csv type that takes a string like "apple,banana,cherry" and turns it into an array ['apple', 'banana', 'cherry'].
// Do this once when your application starts
createSchema.addType('csv', (context) => {
if (context.value === undefined || context.value === null) {
return [];
}
if (typeof context.value !== 'string') {
// Use the public context method to throw a standardized type error
context.throwTypeError();
}
// Trim whitespace from each item
return context.value.split(',').map(item => item.trim());
});
// Now use your new 'csv' type
const productSchema = createSchema({
name: { type: 'string', required: true },
tags: { type: 'csv' }
});
const product = { name: 'Laptop', tags: ' electronics, computers, tech ' };
const { validatedObject } = productSchema.create(product);
// validatedObject.tags will be: ['electronics', 'computers', 'tech']
console.log(validatedObject.tags);Creating an Isolated Schema Factory
Sometimes you want local custom types or validators without mutating the default createSchema factory for the whole application.
Use createSchemaFactory() for that:
import { createSchemaFactory } from 'json-rest-schema'
const adminSchemaFactory = createSchemaFactory()
adminSchemaFactory.addType('admin-prefix-string', context => {
return `admin-${context.value}`
})
const adminSchema = adminSchemaFactory({
name: { type: 'string', required: true },
internalCode: { type: 'admin-prefix-string' }
})
const { validatedObject } = adminSchema.create({
name: ' Alice ',
internalCode: 'ops'
})
// Built-in handlers still work.
console.log(validatedObject.name) // 'Alice'
// Custom handlers stay local to this factory.
console.log(validatedObject.internalCode) // 'admin-ops'createSchemaFactory() installs the built-in core handlers by default so it is usable out of the box.
If you really want a completely bare registry, make that explicit:
import { createSchemaFactory } from 'json-rest-schema'
const bareFactory = createSchemaFactory({ installCore: false })That mode is useful only when you intentionally want to provide every type and validator yourself.