Skip to content

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., the 5 in min: 5).
  • mode: The active validation contract. Preserved as a compatibility alias for operation.
  • 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 standardized TYPE_CAST_FAILED error. 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 error code, a message, and an optional params object.

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.

javascript
// 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'].

javascript
// 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:

javascript
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:

javascript
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.


GPL-3.0-only