Skip to main content

Custom Rules

Extend ValidlyJS with custom validation rules to handle specialized validation logic for your application.

Overview

Custom rules allow you to define specialized validation logic that can be used across different validation formats (Fluent, String, and Array). You can register rules globally or at the instance level.

Note: Custom rules are ideal for encapsulating complex or application-specific validation requirements, such as API-based checks or custom patterns.

Global Registration

Register custom rules globally using Validator.extend to make them available to all validator instances.

// Register a custom rule globally (before instance creation)
// with the global rules
import { extend } from 'validlyjs';

extend('even', {
validate: (value: any) => {
return typeof value === 'number' && value % 2 === 0;
},
message: 'The :field must be an even number'
});

// or statically
import { Validator } from 'validlyjs';

Validator.extend('phone', {
validate: (value, parameters, field, data) => {
const phoneRegex = /^\+?[\d\s\-\(\)]+$/;
return phoneRegex.test(value);
},
message: 'The {field} field must be a valid phone number'
});

// Before the instance
const validator = new Validator(schema);

// Register with parameter support
Validator.extend('strongPassword', {
validate: (value, parameters, field, data) => {
const minLength = parameters[0] || 8;
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasNumber = /\d/.test(value);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(value);

return value.length >= minLength && hasUpper && hasLower && hasNumber && hasSpecial;
},
message: 'The {field} field must be at least {0} characters and contain uppercase, lowercase, number, and special character'
});

// Register async custom rule
Validator.extend('uniqueEmail', {
async validate(value, parameters, field, data) {
const response = await fetch(`/api/check-unique-email?email=${value}`);
const result = await response.json();
return result.isUnique;
},
message: 'The {field} has already been taken'
});

Tip: Use global registration for rules that will be reused across multiple validators.

Instance-Level Registration

Register custom rules for a specific validator instance using the extend method.

const validator = new Validator({
username: "required|string|customUsername:3"
});

// Add custom rule to specific validator instance
validator.extend('customUsername', {
validate: (value, parameters, field, data) => {
const minLength = parameters[0] || 2;
return value.length >= minLength && !/\s/.test(value) && /^[a-zA-Z0-9_]+$/.test(value);
},
message: 'The {field} must be at least {0} characters, contain no spaces, and only alphanumeric characters and underscores'
});

// you may need to refresh the instance whenever you register another custom rule globally
Validator.extend('custom_even', customRule);
validator.refresh(); // Clear caches to pick up new rule

Note: Instance-level custom rules are only available for that specific validator instance. Also, you may need to refresh the instance whenever you register another custom rule globally.

Custom Rules in Different Formats

String Format

const validator = new Validator({
phone: "required|phone",
password: "required|strongPassword:12",
email: "required|email|uniqueEmail",
username: "required|customUsername:5"
});

Array Format

const validator = new Validator({
phone: ["required", "phone"],
password: ["required", "strongPassword:12"],
email: ["required", "email", "uniqueEmail"],
username: ["required", "customUsername:5"]
});

Fluent Format

const validator = new Validator({
phone: string().required().custom('phone'),
password: string().required().custom('strongPassword', [12]),
email: string().required().email().custom('uniqueEmail'),
username: string().required().custom('customUsername', [5])
});

Advanced Custom Rules

Rule with Multiple Parameters

Validator.extend('between', {
validate: (value, parameters, field, data) => {
const min = parseFloat(parameters[0]);
const max = parseFloat(parameters[1]);
const numValue = parseFloat(value);
return numValue >= min && numValue <= max;
},
message: 'The {field} must be between {0} and {1}'
});

// Usage
const validator = new Validator({
score: "number|between:0,100",
rating: ["number", "between:1,5"],
percentage: number().custom('between', [0, 100])
});

Rule with Field Dependencies

Validator.extend('matchField', {
validate: (value, parameters, field, data) => {
const targetField = parameters[0];
return value === data[targetField];
},
message: 'The {field} must match {0}'
});

// Usage
const validator = new Validator({
password: "required|string|min:8",
password_confirmation: "required|matchField:password"
});

Rule with Complex Logic

Validator.extend('conditionalRequired', {
validate: (value, parameters, field, data) => {
const conditionField = parameters[0];
const conditionValue = parameters[1];

// If condition is met, field is required
if (data[conditionField] === conditionValue) {
return value !== null && value !== undefined && value !== '';
}

// Otherwise, field is optional
return true;
},
message: 'The {field} field is required when {0} is {1}'
});

// Usage
const validator = new Validator({
country: "string",
state: "conditionalRequired:country,USA"
});

Custom Rules with Union Types

Validator.extend('flexibleId', {
validate: (value, parameters, field, data) => {
// Accept either UUID or positive integer
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
const isPositiveInt = Number.isInteger(Number(value)) && Number(value) > 0;
return isUuid || isPositiveInt;
},
message: 'The {field} must be a valid UUID or positive integer'
});

// Usage in union
const validator = new Validator({
id: union([
string().uuid(),
number().positive().integer(),
string().custom('flexibleId')
])
});

Tip: Custom rules in union types allow for flexible validation of fields that can accept multiple data types or formats.