Object schema validation library for JavaScript with comprehensive validation capabilities
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Extension system for creating custom schema types, validation rules, and modifying default behavior.
Adds custom schema types and validation rules to joi instances.
/**
* Adds custom schema types and validation rules
* @param extensions - One or more extension definitions
* @returns New joi instance with added extensions
*/
function extend(...extensions: Extension[]): Root;
interface Extension {
// Extension identification
type: string | RegExp; // Extension type name or pattern
base?: Schema; // Base schema to extend from
// Custom messages
messages?: Record<string, string>;
// Value processing hooks
coerce?: (value: any, helpers: CustomHelpers) => CoerceResult;
pre?: (value: any, helpers: CustomHelpers) => any;
// Validation rules
rules?: Record<string, RuleOptions>;
// Schema behavior overrides
overrides?: Record<string, any>;
// Schema rebuilding
rebuild?: (schema: Schema) => Schema;
// Manifest support
manifest?: ManifestOptions;
// Constructor arguments handling
args?: (schema: Schema, ...args: any[]) => Schema;
// Schema description modification
describe?: (description: SchemaDescription) => SchemaDescription;
// Internationalization
language?: Record<string, string>;
}
interface RuleOptions {
// Rule configuration
method?: (...args: any[]) => Schema;
validate?: (value: any, helpers: CustomHelpers, args: any) => any;
args?: (string | RuleArgOptions)[];
// Rule behavior
multi?: boolean; // Allow multiple rule applications
priority?: boolean; // Execute rule with priority
manifest?: boolean; // Include in manifest
// Rule conversion
convert?: boolean; // Enable value conversion
}
interface CustomHelpers {
// Error generation
error(code: string, local?: any): ErrorReport;
// Schema access
schema: Schema;
state: ValidationState;
prefs: ValidationOptions;
// Original value
original: any;
// Validation utilities
warn(code: string, local?: any): void;
message(messages: LanguageMessages, local?: any): string;
}
interface CoerceResult {
value?: any;
errors?: ErrorReport[];
}Usage Examples:
const Joi = require('joi');
// Simple extension for credit card validation
const creditCardExtension = {
type: 'creditCard',
base: Joi.string(),
messages: {
'creditCard.invalid': '{{#label}} must be a valid credit card number'
},
rules: {
luhn: {
validate(value, helpers) {
// Luhn algorithm implementation
const digits = value.replace(/\D/g, '');
let sum = 0;
let isEven = false;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = parseInt(digits[i]);
if (isEven) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
isEven = !isEven;
}
if (sum % 10 !== 0) {
return helpers.error('creditCard.invalid');
}
return value;
}
}
}
};
// Create extended joi instance
const extendedJoi = Joi.extend(creditCardExtension);
// Use the new credit card type
const schema = extendedJoi.creditCard().luhn();
const { error } = schema.validate('4532015112830366'); // Valid Visa number// Email domain extension with custom rules
const emailDomainExtension = {
type: 'emailDomain',
base: Joi.string(),
messages: {
'emailDomain.blockedDomain': '{{#label}} domain {{#domain}} is not allowed',
'emailDomain.requiredDomain': '{{#label}} must use domain {{#domain}}'
},
coerce(value, helpers) {
if (typeof value === 'string') {
return { value: value.toLowerCase().trim() };
}
return { value };
},
rules: {
allowDomains: {
method(domains) {
return this.$_addRule({ name: 'allowDomains', args: { domains } });
},
args: [
{
name: 'domains',
assert: Joi.array().items(Joi.string()).min(1),
message: 'must be an array of domain strings'
}
],
validate(value, helpers, { domains }) {
const emailDomain = value.split('@')[1];
if (!domains.includes(emailDomain)) {
return helpers.error('emailDomain.requiredDomain', { domain: domains.join(', ') });
}
return value;
}
},
blockDomains: {
method(domains) {
return this.$_addRule({ name: 'blockDomains', args: { domains } });
},
args: [
{
name: 'domains',
assert: Joi.array().items(Joi.string()).min(1),
message: 'must be an array of domain strings'
}
],
validate(value, helpers, { domains }) {
const emailDomain = value.split('@')[1];
if (domains.includes(emailDomain)) {
return helpers.error('emailDomain.blockedDomain', { domain: emailDomain });
}
return value;
}
}
}
};
const customJoi = Joi.extend(emailDomainExtension);
// Use custom email domain validation
const emailSchema = customJoi.emailDomain()
.allowDomains(['company.com', 'partner.org'])
.blockDomains(['competitor.com']);Creates a new joi instance with modified default schema behavior.
/**
* Creates new joi instance with default schema modifiers
* @param modifier - Function that modifies default schemas
* @returns New joi instance with modified defaults
*/
function defaults(modifier: (schema: AnySchema) => AnySchema): Root;Usage Examples:
// Create joi instance with stricter defaults
const strictJoi = Joi.defaults((schema) => {
return schema.options({
abortEarly: false, // Collect all errors
allowUnknown: false, // Disallow unknown keys
stripUnknown: true // Strip unknown keys
});
});
// All schemas created with strictJoi will have these defaults
const schema = strictJoi.object({
name: strictJoi.string().required(),
age: strictJoi.number()
});
// Custom defaults for specific needs
const apiJoi = Joi.defaults((schema) => {
return schema
.options({ convert: false }) // Disable type conversion
.strict(); // Enable strict mode
});// Extension that applies to multiple schema types
const timestampExtension = {
type: /^(string|number)$/, // Apply to string and number types
rules: {
timestamp: {
method(format = 'unix') {
return this.$_addRule({ name: 'timestamp', args: { format } });
},
args: [
{
name: 'format',
assert: Joi.string().valid('unix', 'javascript'),
message: 'must be "unix" or "javascript"'
}
],
validate(value, helpers, { format }) {
const timestamp = format === 'unix' ? value * 1000 : value;
const date = new Date(timestamp);
if (isNaN(date.getTime())) {
return helpers.error('timestamp.invalid');
}
return value;
}
}
},
messages: {
'timestamp.invalid': '{{#label}} must be a valid timestamp'
}
};// Extension factory function
const createValidationExtension = (validatorName, validatorFn) => {
return {
type: 'any',
rules: {
[validatorName]: {
method(...args) {
return this.$_addRule({
name: validatorName,
args: { params: args }
});
},
validate(value, helpers, { params }) {
const isValid = validatorFn(value, ...params);
if (!isValid) {
return helpers.error(`${validatorName}.invalid`);
}
return value;
}
}
},
messages: {
[`${validatorName}.invalid`]: `{{#label}} failed ${validatorName} validation`
}
};
};
// Create custom validators
const divisibleByExtension = createValidationExtension(
'divisibleBy',
(value, divisor) => value % divisor === 0
);
const extendedJoi = Joi.extend(divisibleByExtension);
const schema = extendedJoi.number().divisibleBy(5);interface CustomHelpers {
/**
* Creates validation error
* @param code - Error code for message lookup
* @param local - Local context variables
* @returns ErrorReport object
*/
error(code: string, local?: any): ErrorReport;
/**
* Creates warning (non-fatal error)
* @param code - Warning code for message lookup
* @param local - Local context variables
*/
warn(code: string, local?: any): void;
/**
* Formats message with local context
* @param messages - Message templates
* @param local - Local context variables
* @returns Formatted message string
*/
message(messages: LanguageMessages, local?: any): string;
// Context properties
schema: Schema; // Current schema being validated
state: ValidationState; // Current validation state
prefs: ValidationOptions; // Current preferences
original: any; // Original input value
}
interface ValidationState {
key?: string; // Current validation key
path: (string | number)[]; // Path to current value
parent?: any; // Parent object
reference?: any; // Reference context
ancestors?: any[]; // Ancestor objects
}Usage Examples:
const advancedExtension = {
type: 'advanced',
base: Joi.any(),
rules: {
customValidation: {
validate(value, helpers) {
// Access validation context
const path = helpers.state.path.join('.');
const parent = helpers.state.parent;
const prefs = helpers.prefs;
// Custom validation logic
if (value === 'invalid') {
return helpers.error('advanced.invalid', {
path,
value
});
}
// Issue warning for suspicious values
if (value === 'suspicious') {
helpers.warn('advanced.suspicious', { value });
}
return value;
}
}
},
messages: {
'advanced.invalid': 'Value {{#value}} at path {{#path}} is invalid',
'advanced.suspicious': 'Value {{#value}} might be suspicious'
}
};const preprocessingExtension = {
type: 'preprocessed',
base: Joi.string(),
// Pre-process values before validation
pre(value, helpers) {
if (typeof value === 'string') {
// Normalize whitespace and case
return value.trim().toLowerCase();
}
return value;
},
// Coerce values during validation
coerce(value, helpers) {
if (typeof value === 'number') {
return { value: value.toString() };
}
if (Array.isArray(value)) {
return {
errors: [helpers.error('preprocessed.notString')]
};
}
return { value };
},
messages: {
'preprocessed.notString': '{{#label}} cannot be converted to string'
}
};// Combine multiple extensions
const compositeJoi = Joi
.extend(creditCardExtension)
.extend(emailDomainExtension)
.extend(timestampExtension);
// Use multiple custom types together
const complexSchema = compositeJoi.object({
email: compositeJoi.emailDomain().allowDomains(['trusted.com']),
creditCard: compositeJoi.creditCard().luhn(),
timestamp: compositeJoi.number().timestamp('unix')
});