CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-joi

Object schema validation library for JavaScript with comprehensive validation capabilities

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

extensions-customization.mddocs/

Extensions and Customization

Extension system for creating custom schema types, validation rules, and modifying default behavior.

Capabilities

Extend Function

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

Advanced Extension Example

// 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']);

Defaults Function

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 Patterns

Multi-Type Extensions

// 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'
    }
};

Function-Based Extensions

// 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);

Extension Helpers

Custom Helper Utilities

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'
    }
};

Pre-Processing and Coercion

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'
    }
};

Extension Composition

// 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')
});

docs

extensions-customization.md

index.md

references-expressions.md

schema-types.md

utility-functions.md

validation-methods.md

tile.json