Joi

repository·master·Indexed 12 days ago

https://github.com/hapijs/joi

A powerful schema description language and data validator for JavaScript used to ensure data integrity. Joi allows developers to define immutable schemas using types like Joi.string() and Joi.object() and validate data against them using schema.validate() or Joi.assert(). Version 18.2.3 supports advanced features including dynamic expressions via Joi.expression(), relative references with Joi.ref(), and custom validation logic through any.custom().

Tokens
33.2K
Snippets
163
Records
178
Agent score
96%

What's inside Joi

  1. How Joi processes input values during validation

    master

    When validate() is called, Joi follows a specific execution order. Understanding this order is crucial when writing extensions, as extending schemas does not change this sequence:

    1. Schema Generation: Generates a new schema if dynamic rules like when() or link() are present.
    2. Option Merging: Merges validation options with those from any.prefs().
    3. Caching: Returns the result immediately if caching is enabled and the value is found.
    4. prepare: Runs the prepare method (if defined). Errors here abort validation immediately.
    5. coerce: Coerces the value (if convert is enabled). Errors here abort validation immediately.
    6. Empty Check: If the value matches any.empty(), it is converted to undefined.
    7. Presence Validation: Validates required/optional presence.
    8. Allowed/Valid/Invalid: Validates against allowed/invalid value sets.
    9. validate: Runs base validation. Errors here abort validation immediately.
    10. Rules: Runs specific validation rules.
  2. Understand the ValidationError object

    master

    When validation fails, Joi throws or returns a ValidationError object. This object provides detailed information about why the validation failed, allowing you to programmatically inspect errors or present user-friendly messages.

    Key properties of a ValidationError:

    • name: Always 'ValidationError'.
    • isJoi: Always true.
    • details: An array of error objects, each containing:
      • message: A string describing the error.
      • path: An ordered array of accessors (keys/indices) pointing to the location of the error.
      • type: The specific error code (e.g., string.min, number.base).
      • context: An object providing metadata specific to the error type (e.g., the limit for a length error or the value that failed).
    • annotate(): A method that returns a string representation of the input object with visual annotations (colors) pointing to the error locations. Passing a truthy value to annotate(true) will strip the ANSI colors.
  3. How links work for recursive schemas

    master

    The link(ref) method allows you to link to another schema node, which is essential for creating recursive schemas. Links can be expressed in three ways:

    1. Named links: Use .id('name') on a schema and Joi.link('#name') to reference it. This is the recommended approach for clarity.
    2. Relative links: Use dot notation (e.g., Joi.link('...') to go up levels) to navigate the schema tree.
    3. Absolute links: Use a path starting from the root (e.g., Joi.link('/')).

    Important Safety Note: Always use link.maxRecursion(limit) on recursive links to prevent deeply nested inputs from causing stack overflows. If the limit is exceeded, Joi returns a link.maxRecursion error. If the validation exceeds the runtime call stack during resolution, it returns a link.depth error.

    Possible validation errors: link.depth, link.maxRecursion.

    // Named link example (Recommended)
    const person = Joi.object({
        firstName: Joi.string().required(),
        lastName: Joi.string().required(),
        children: Joi.array().items(Joi.link('#person'))
      }).id('person');
    
    // Relative link example
    const person = Joi.object({
        firstName: Joi.string().required(),
        lastName: Joi.string().required(),
        children: Joi.array().items(Joi.link('...'))
    });
    
    // Using maxRecursion for safety
    const schema = Joi.object({
        name: Joi.string().required(),
        keys: Joi.array().items(Joi.link('...').maxRecursion(10))
    });
  4. How relative references work in Joi

    master

    By default, a reference is relative to the parent of the current value.

    You can navigate the hierarchy using the . separator:

    • . : Self
    • .. : Parent (default behavior)
    • ... : Grandparent
    • .... : Great-grandparent

    Alternatively, use the ancestor option to set the starting point numerically:

    • 0: Self
    • 1: Parent (default)
    • 2: Grandparent
    • 3: Great-grandparent

    Note: If a reference attempts to reach beyond the value root, validation will fail.

    // Using separators
    {
        x: {
            a: Joi.any(),
            b: {
                c: Joi.any(),
                d: Joi.ref('c'),      // Sibling
                e: Joi.ref('...a'),   // Grandparent
                f: Joi.ref('....y')   // Great-grandparent
            }
        },
        y: Joi.any()
    }
    
    // Using ancestor option
    {
        x: {
            a: Joi.any(),
            b: {
                c: Joi.any(),
                d: Joi.ref('c', { ancestor: 1 }),
                e: Joi.ref('a', { ancestor: 2 }),
                f: Joi.ref('y', { ancestor: 3 })
            }
        },
        y: Joi.any()
    }
  5. Apply conditional logic with any.when()

    master

    Adds conditions that modify the schema before it is applied to the value. This is useful for dynamic validation based on other keys or values.

    Parameters:

    • condition: A key name, a reference, or a schema. Defaults to Joi.ref('.') if omitted.
    • options: An object containing:
      • is: The condition expressed as a Joi schema (or a literal that gets compiled). Defaults to allowing undefined unless .required() is used.
      • not: The negative version of is.
      • then: The schema to use if the condition is true.
      • otherwise: The schema to use if the condition is false.
      • switch: An array of { is, then } objects. The last item can include otherwise.
      • break: Stops processing other conditions if this rule matches.

    Key Behaviors:

    • Literal values: If is, then, or otherwise are literals (e.g., 'x'), they are compiled into override schemas (e.g., Joi.valid('x')). To append a value rather than override, use Joi.valid('x') explicitly.
    • Performance: Because schemas are constructed at validation time, there is a performance impact. Run-time generated schemas are cached after the first generation.
    • Reference conditions: If is, not, and switch are missing, is defaults to checking for truthiness (Joi.invalid(null, false, 0, '').required()).
    // Example: Validating 'a' based on 'b' and 'c'
    const schema = {
        a: Joi.any()
            .valid('x')
            .when('b', { is: Joi.exist(), then: Joi.valid('y'), otherwise: Joi.valid('z') })
            .when('c', { is: Joi.number().min(10), then: Joi.forbidden() }),
        b: Joi.any(),
        c: Joi.number()
    };
    
    // Example: Using switch for multiple values on one reference
    const schemaSwitch = Joi.object({
        a: Joi.number().required(),
        b: Joi.number()
            .when('a', {
                switch: [
                    { is: 0, then: Joi.valid(1) },
                    { is: 1, then: Joi.valid(2) },
                    { is: 2, then: Joi.valid(3) }
                ],
                otherwise: Joi.valid(4)
            })
    });
    
    // Shorthand switch syntax
    const schemaSwitchShort = Joi.object({
        a: Joi.number().required(),
        b: Joi.number()
            .when('a', [
                { is: 0, then: 1 },
                { is: 1, then: 2 },
                { is: 2, then: 3, otherwise: 4 }
            ])
    });
  6. Run Joi performance benchmarks

    master

    The benchmarks in this repository are designed for performance regression testing to ensure modifications do not negatively impact Joi's performance. They are not intended for comparing Joi against other libraries.

    To use the benchmarks:

    1. Establish a baseline: Run npm run bench-update to create the initial performance baseline.
    2. Run comparison: Run npm run bench or npm test to compare your current changes against the established baseline.

    In the generated report, significant performance changes (greater than 10% by default) are highlighted in color.

    # Establish a baseline
    npm run bench-update
    
    # Compare modifications to the baseline
    npm run bench
    # OR
    npm test
  7. Get started with Joi validation

    master

    Joi allows you to describe data structures using a readable language. Validation is a two-step process:

    1. Construct a schema: Use Joi types (like Joi.string(), Joi.object()) and constraints (like .min(), .required()). Note that Joi schemas are immutable; every rule added returns a new schema object.
    2. Validate a value: Use schema.validate(value) to check a value against the schema.

    If the input is valid, error will be undefined. If invalid, error will be a ValidationError object.

    By default, keys in an object schema are optional. To make them mandatory, use .required() on the schema or pass { presence: 'required' } in the validation options.

    const Joi = require('joi');
    
    const schema = Joi.object({
        username: Joi.string()
            .alphanum()
            .min(3)
            .max(30)
            .required(),
        birth_year: Joi.number()
            .integer()
            .min(1900)
            .max(2013)
    }).with('username', 'birth_year');
    
    // Synchronous validation
    const { error, value } = schema.validate({ username: 'abc', birth_year: 1994 });
    
    // Asynchronous validation
    try {
        const value = await schema.validateAsync({ username: 'abc', birth_year: 1994 });
    } catch (err) { /* handle error */ }
  8. Use alternatives to validate against multiple schemas

    master

    The alternatives type allows you to validate a value against multiple possible schemas. You can use the .try() method to define these alternatives. By default, Joi uses an any match mode, meaning the value only needs to match at least one of the provided schemas.

    To change the matching behavior, use the .match(mode) method with one of the following modes:

    • 'any' (default): The value must match at least one schema.
    • 'one': The value must match exactly one schema.
    • 'all': The value must match all provided schemas.

    Note: You cannot combine .match() modes with conditional rules (like .when()).

    const Joi = require('joi');
    
    // Match any (default behavior)
    const schema = Joi.alternatives().try(Joi.string(), Joi.number());
    
    // Match exactly one
    const schemaOne = Joi.alternatives().match('one').try(Joi.string(), Joi.number());
    
    // Match all
    const schemaAll = Joi.alternatives().match('all').try(Joi.string(), Joi.number());
  9. Validate and manipulate dates with Joi

    master

    The date type in Joi allows you to validate that a value is a valid JavaScript Date object or can be coerced into one. It supports various formats including ISO 8601, JavaScript timestamps (milliseconds), and Unix timestamps (seconds).

    Supported formats:

    • iso: ISO 8601 strings.
    • javascript: Milliseconds since epoch (number or string).
    • unix: Seconds since epoch (number or string).

    When using convert: true (the default), Joi can coerce strings or numbers into Date objects based on the specified format.

    const Joi = require('joi');
    
    // Validate ISO 8601 strings
    const schema = Joi.date().iso();
    const { value } = schema.validate('2023-01-01T00:00:00Z');
    
    // Validate Unix timestamps (seconds)
    const unixSchema = Joi.date().timestamp('unix');
    const { value: unixValue } = unixSchema.validate('1672531200');
  10. Validate numbers with Joi

    master

    Use Joi.number() to create a schema for validating numeric values. By default, Joi will attempt to coerce strings that represent valid numbers into actual number types.

    Note that Joi considers numbers outside the range of Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER to be "unsafe" by default, which may trigger a number.unsafe error unless the .unsafe() flag is enabled.

    const Joi = require('joi');
    
    const schema = Joi.number().integer().min(1);
    const { value, error } = schema.validate('10');
    // value is 10 (coerced from string)