Logo
⚠️ Unsaved
[M]:

Joi Complete Guide: Building and Validating Schemas

This notebook provides a comprehensive guide to Joi, the powerful schema description language and data validator for JavaScript. We'll explore advanced schema building techniques, validation strategies, and real-world examples to help you implement robust data validation in your Node.js applications.

[52]:
// Install Joi using npm
!npm install joi
$ npm install joi

up to date in 425ms

28 packages are looking for funding
  run `npm fund` for details
[53]:
// Import Joi
const Joi = require('joi');

// Check if Joi is loaded correctly
console.log("Joi version:", Joi.version, "\n");
Joi version: 17.13.3 
[M]:

1. Schema Building Fundamentals

Let's start by understanding the core components of Joi schemas and how to build them effectively.

[54]:
// Basic schema types
const stringSchema = Joi.string();
const numberSchema = Joi.number();
const booleanSchema = Joi.boolean();
const dateSchema = Joi.date();
const arraySchema = Joi.array();
const objectSchema = Joi.object();

console.log("Basic schema types:\n");
console.log("String schema:", stringSchema.describe(), "\n");
console.log("Number schema:", numberSchema.describe(), "\n");
console.log("Boolean schema:", booleanSchema.describe(), "\n");
Basic schema types:
String schema: { type: 'string' } 
Number schema: { type: 'number' } 
Boolean schema: { type: 'boolean' } 
[55]:
// Building a complex user schema
const userSchema = Joi.object({
id: Joi.string().guid({ version: 'uuidv4' }),
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email({ tlds: { allow: false } }).required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{8,30}$')).required(),
birthYear: Joi.number().integer().min(1900).max(new Date().getFullYear()),
createdAt: Joi.date().default(Date.now),
roles: Joi.array().items(Joi.string().valid('user', 'admin', 'editor')).default(['user'])
});

console.log("Complex user schema structure:\n");
console.log(JSON.stringify(userSchema.describe(), null, 2), "\n");
Complex user schema structure:
{
  "type": "object",
  "keys": {
    "id": {
      "type": "string",
      "rules": [
        {
          "name": "guid",
          "args": {
            "options": {
              "version": "uuidv4"
            }
          }
        }
      ]
    },
    "username": {
      "type": "string",
      "flags": {
        "presence": "required"
      },
      "rules": [
        {
          "name": "alphanum"
        },
        {
          "name": "min",
          "args": {
            "limit": 3
          }
        },
        {
          "name": "max",
          "args": {
            "limit": 30
          }
        }
      ]
    },
    "email": {
      "type": "string",
      "flags": {
        "presence": "required"
      },
      "rules": [
        {
          "name": "email",
          "args": {
            "options": {
              "tlds": {
                "allow": false
              }
            }
          }
        }
      ]
    },
    "password": {
      "type": "string",
      "flags": {
        "presence": "required"
      },
      "rules": [
        {
          "name": "pattern",
          "args": {
            "regex": "/^[a-zA-Z0-9]{8,30}$/"
          }
        }
      ]
    },
    "birthYear": {
      "type": "number",
      "rules": [
        {
          "name": "integer"
        },
        {
          "name": "min",
          "args": {
            "limit": 1900
          }
        },
        {
          "name": "max",
          "args": {
            "limit": 2025
          }
        }
      ]
    },
    "createdAt": {
      "type": "date",
      "flags": {}
    },
    "roles": {
      "type": "array",
      "flags": {
        "default": [
          "user"
        ]
      },
      "items": [
        {
          "type": "string",
          "flags": {
            "only": true
          },
          "allow": [
            "user",
            "admin",
            "editor"
          ]
        }
      ]
    }
  }
} 
[M]:

2. Schema Composition and Reuse

Learn how to compose and reuse schemas for better maintainability.

[56]:
// Define reusable schema components
const schemas = {
id: Joi.string().guid({ version: 'uuidv4' }),
email: Joi.string().email({ tlds: { allow: false } }).required(),
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required(),
timestamp: Joi.date().default(Date.now)
};
[57]:
// Compose schemas using the reusable components
const loginSchema = Joi.object({
email: schemas.email,
password: schemas.password,
rememberMe: Joi.boolean().default(false)
});

const registrationSchema = Joi.object({
username: schemas.username,
email: schemas.email,
password: schemas.password,
confirmPassword: Joi.valid(Joi.ref('password')).required()
.messages({ 'any.only': 'Passwords must match' }),
createdAt: schemas.timestamp
});

// Test the login schema
const loginData = {
email: 'user@example.com',
password: 'Password123'
};

const loginResult = loginSchema.validate(loginData);
console.log("Login validation result:\n");
console.log(loginResult, "\n");
Login validation result:
{
  value: {
    email: 'user@example.com',
    password: 'Password123',
    rememberMe: false
  }
} 
[58]:
// Test the registration schema
const registrationData = {
username: 'johndoe',
email: 'john@example.com',
password: 'Password123',
confirmPassword: 'Password123'
};

const registrationResult = registrationSchema.validate(registrationData);
console.log("Registration validation result:\n");
console.log(registrationResult, "\n");

// Notice how createdAt was automatically added with the default value
console.log("Created timestamp type:", typeof registrationResult.value.createdAt, "\n");
Registration validation result:
{
  value: {
    username: 'johndoe',
    email: 'john@example.com',
    password: 'Password123',
    confirmPassword: 'Password123',
    createdAt: 1741806806068
  }
} 
Created timestamp type: number 
[M]:

3. Advanced String Validation

Explore advanced string validation techniques for common use cases.

[59]:
// Advanced string validation examples
const advancedStringSchemas = {
// URL validation
url: Joi.string().uri({
scheme: ['http', 'https']
}),
// Credit card validation
creditCard: Joi.string().creditCard(),
// Hexadecimal validation
hexColor: Joi.string().hex().length(7),
// Base64 validation
base64: Joi.string().base64(),
// IP address validation
ipAddress: Joi.string().ip({ version: ['ipv4', 'ipv6'] }),
// Pattern validation with regex
zipCode: Joi.string().pattern(/^\d{5}(-\d{4})?$/),
// Phone number validation
phoneNumber: Joi.string().pattern(/^\+?[1-9]\d{1,14}$/)
};
[60]:
// Test URL validation
console.log("URL validation:\n");
console.log("Valid URL:", advancedStringSchemas.url.validate('https://example.com'), "\n");
console.log("Invalid URL:", advancedStringSchemas.url.validate('ftp://example.com'), "\n");
URL validation:
Valid URL: { value: 'https://example.com' } 
Invalid URL: {
  value: 'ftp://example.com',
  error: [Error [ValidationError]: "value" must be a valid uri with a scheme matching the http|https pattern] {
    _original: 'ftp://example.com',
    details: [ [Object] ]
  }
} 
[61]:
// Test credit card validation
console.log("Credit card validation:\n");
console.log("Valid card:", advancedStringSchemas.creditCard.validate('4111111111111111'), "\n");
console.log("Invalid card:", advancedStringSchemas.creditCard.validate('1234567812345678'), "\n");
Credit card validation:
Valid card: { value: '4111111111111111' } 
Invalid card: {
  value: '1234567812345678',
  error: [Error [ValidationError]: "value" must be a credit card] {
    _original: '1234567812345678',
    details: [ [Object] ]
  }
} 
[M]:

4. Number and Date Validation

Explore advanced number and date validation techniques.

[62]:
// Advanced number validation
const numberSchemas = {
// Integer validation
integer: Joi.number().integer(),
// Positive number validation
positive: Joi.number().positive(),
// Negative number validation
negative: Joi.number().negative(),
// Range validation
range: Joi.number().min(0).max(100),
// Precision validation (decimal places)
price: Joi.number().precision(2),
// Port number validation
port: Joi.number().port(),
// Multiple of validation
multipleOf5: Joi.number().multiple(5)
};
[63]:
// Test number validation
console.log("Number validation examples:\n");
console.log("Integer validation:", numberSchemas.integer.validate(42), "\n");
console.log("Integer validation (invalid):", numberSchemas.integer.validate(42.5), "\n");
console.log("Precision validation:", numberSchemas.price.validate(99.99), "\n");
console.log("Multiple of 5 validation:", numberSchemas.multipleOf5.validate(25), "\n");
console.log("Multiple of 5 validation (invalid):", numberSchemas.multipleOf5.validate(27), "\n");
Number validation examples:
Integer validation: { value: 42 } 
Integer validation (invalid): {
  value: 42.5,
  error: [Error [ValidationError]: "value" must be an integer] {
    _original: 42.5,
    details: [ [Object] ]
  }
} 
Precision validation: { value: 99.99 } 
Multiple of 5 validation: { value: 25 } 
Multiple of 5 validation (invalid): {
  value: 27,
  error: [Error [ValidationError]: "value" must be a multiple of 5] {
    _original: 27,
    details: [ [Object] ]
  }
} 
[64]:
// Advanced date validation
const dateSchemas = {
// Current or past date
pastOrPresent: Joi.date().max('now'),
// Future date
future: Joi.date().min('now'),
// Date range
dateRange: Joi.date().min('2020-01-01').max('2029-12-31'),
// ISO date format
isoDate: Joi.date().iso(),
// Timestamp validation
timestamp: Joi.date().timestamp(),
// Date with time constraints
businessHours: Joi.date().custom((value, helpers) => {
const hours = value.getHours();
if (hours < 9 || hours >= 17) {
return helpers.error('date.businessHours');
}
return value;
}, 'business hours validation')
};
[65]:
// Test date validation
console.log("Date validation examples:\n");

const pastDate = new Date('2020-01-01');
console.log("Past date validation:", dateSchemas.pastOrPresent.validate(pastDate), "\n");

const futureDate = new Date();
futureDate.setFullYear(futureDate.getFullYear() + 1);
console.log("Future date validation:", dateSchemas.future.validate(futureDate), "\n");

console.log("ISO date validation:", dateSchemas.isoDate.validate('2023-05-15T14:30:00Z'), "\n");
Date validation examples:
Past date validation: { value: 2020-01-01T00:00:00.000Z } 
Future date validation: { value: 2026-03-12T19:13:32.678Z } 
ISO date validation: { value: 2023-05-15T14:30:00.000Z } 
[M]:

5. Object Schema Advanced Techniques

Explore advanced techniques for object schema validation.

[66]:
// Object schema with pattern keys
const metadataSchema = Joi.object({
// Fixed keys
id: Joi.string().required(),
name: Joi.string().required(),
// Pattern keys for dynamic metadata
// Any key starting with 'meta_' must have a string value
}).pattern(/^meta_/, Joi.string());

// Test metadata schema
const metadata = {
id: '123',
name: 'Test Item',
meta_color: 'blue',
meta_size: 'large',
meta_material: 'cotton'
};

const metadataResult = metadataSchema.validate(metadata);
console.log("Metadata validation with pattern keys:\n");
console.log(metadataResult, "\n");
Metadata validation with pattern keys:
{
  value: {
    id: '123',
    name: 'Test Item',
    meta_color: 'blue',
    meta_size: 'large',
    meta_material: 'cotton'
  }
} 
[67]:
// Object schema with unknown keys handling
const strictSchema = Joi.object({
name: Joi.string().required(),
age: Joi.number().integer().min(0)
}).strict(); // No unknown keys allowed

const strictData = {
name: 'John',
age: 30,
country: 'USA' // Unknown key
};

const strictResult = strictSchema.validate(strictData);
console.log("Strict schema validation (unknown keys rejected):\n");
console.log(strictResult, "\n");
Strict schema validation (unknown keys rejected):
{
  value: { name: 'John', age: 30, country: 'USA' },
  error: [Error [ValidationError]: "country" is not allowed] {
    _original: { name: 'John', age: 30, country: 'USA' },
    details: [ [Object] ]
  }
} 
[68]:
// Object schema with unknown keys allowed
const flexibleSchema = Joi.object({
name: Joi.string().required(),
age: Joi.number().integer().min(0)
}).unknown(true); // Unknown keys allowed

const flexibleResult = flexibleSchema.validate(strictData);
console.log("Flexible schema validation (unknown keys allowed):\n");
console.log(flexibleResult, "\n");
Flexible schema validation (unknown keys allowed):
{ value: { name: 'John', age: 30, country: 'USA' } } 
[69]:
// Object schema with key dependencies
const paymentSchema = Joi.object({
paymentMethod: Joi.string().valid('credit_card', 'paypal', 'bank_transfer').required(),
// Credit card details (required only when paymentMethod is 'credit_card')
cardNumber: Joi.string().creditCard()
.when('paymentMethod', {
is: 'credit_card',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
expiryDate: Joi.string().pattern(/^\d{2}\/\d{2}$/)
.when('paymentMethod', {
is: 'credit_card',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
// PayPal details (required only when paymentMethod is 'paypal')
paypalEmail: Joi.string().email()
.when('paymentMethod', {
is: 'paypal',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
// Bank transfer details (required only when paymentMethod is 'bank_transfer')
accountNumber: Joi.string()
.when('paymentMethod', {
is: 'bank_transfer',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
routingNumber: Joi.string()
.when('paymentMethod', {
is: 'bank_transfer',
[70]:
// Test payment schema with credit card
const creditCardPayment = {
paymentMethod: 'credit_card',
cardNumber: '4111111111111111',
expiryDate: '12/25'
};

const creditCardResult = paymentSchema.validate(creditCardPayment);
console.log("Credit card payment validation:\n");
console.log(creditCardResult, "\n");
Credit card payment validation:
{
  value: {
    paymentMethod: 'credit_card',
    cardNumber: '4111111111111111',
    expiryDate: '12/25'
  }
} 
[71]:
// Test payment schema with PayPal
const paypalPayment = {
paymentMethod: 'paypal',
paypalEmail: 'user@example.com',
cardNumber: '4111111111111111' // This should be forbidden
};

const paypalResult = paymentSchema.validate(paypalPayment);
console.log("PayPal payment validation (with forbidden field):\n");
console.log(paypalResult, "\n");
PayPal payment validation (with forbidden field):
{
  value: {
    paymentMethod: 'paypal',
    paypalEmail: 'user@example.com',
    cardNumber: '4111111111111111'
  },
  error: [Error [ValidationError]: "cardNumber" is not allowed] {
    _original: {
      paymentMethod: 'paypal',
      paypalEmail: 'user@example.com',
      cardNumber: '4111111111111111'
    },
    details: [ [Object] ]
  }
} 
[M]:

6. Array Schema Advanced Techniques

Explore advanced techniques for array schema validation.

[72]:
// Basic array validation
const tagsSchema = Joi.array().items(Joi.string()).min(1).max(5);

// Test basic array validation
console.log("Basic array validation:\n");
console.log("Valid tags:", tagsSchema.validate(['javascript', 'nodejs', 'validation']), "\n");
console.log("Invalid tags (too many):", tagsSchema.validate(['a', 'b', 'c', 'd', 'e', 'f']), "\n");
Basic array validation:
Valid tags: { value: [ 'javascript', 'nodejs', 'validation' ] } 
Invalid tags (too many): {
  value: [ 'a', 'b', 'c', 'd', 'e', 'f' ],
  error: [Error [ValidationError]: "value" must contain less than or equal to 5 items] {
    _original: [ 'a', 'b', 'c', 'd', 'e', 'f' ],
    details: [ [Object] ]
  }
} 
[73]:
// Array with ordered items
const coordinatesSchema = Joi.array().ordered(
Joi.number().required(), // x coordinate
Joi.number().required(), // y coordinate
Joi.number().optional() // z coordinate (optional)
);

// Test ordered array validation
console.log("Ordered array validation:\n");
console.log("Valid 2D coordinates:", coordinatesSchema.validate([10, 20]), "\n");
console.log("Valid 3D coordinates:", coordinatesSchema.validate([10, 20, 30]), "\n");
console.log("Invalid coordinates (missing y):", coordinatesSchema.validate([10]), "\n");
Ordered array validation:
Valid 2D coordinates: { value: [ 10, 20 ] } 
Valid 3D coordinates: { value: [ 10, 20, 30 ] } 
Invalid coordinates (missing y): {
  value: [ 10 ],
  error: [Error [ValidationError]: "value" does not contain 1 required value(s)] {
    _original: [ 10 ],
    details: [ [Object] ]
  }
} 
[74]:
// Array with mixed item types
const mixedArraySchema = Joi.array().items(
Joi.string(),
Joi.number(),
Joi.boolean()
);

// Test mixed array validation
console.log("Mixed array validation:\n");
console.log("Valid mixed array:", mixedArraySchema.validate(['hello', 42, true, 'world']), "\n");
console.log("Invalid mixed array:", mixedArraySchema.validate(['hello', 42, true, { key: 'value' }]), "\n");
Mixed array validation:
Valid mixed array: { value: [ 'hello', 42, true, 'world' ] } 
Invalid mixed array: {
  value: [ 'hello', 42, true, { key: 'value' } ],
  error: [Error [ValidationError]: "[3]" does not match any of the allowed types] {
    _original: [ 'hello', 42, true, [Object] ],
    details: [ [Object] ]
  }
} 
[75]:
// Array with unique items
const uniqueArraySchema = Joi.array().items(Joi.string()).unique();

// Test unique array validation
console.log("Unique array validation:\n");
console.log("Valid unique array:", uniqueArraySchema.validate(['a', 'b', 'c']), "\n");
console.log("Invalid unique array:", uniqueArraySchema.validate(['a', 'b', 'a']), "\n");
Unique array validation:
Valid unique array: { value: [ 'a', 'b', 'c' ] } 
Invalid unique array: {
  value: [ 'a', 'b', 'a' ],
  error: [Error [ValidationError]: "[2]" contains a duplicate value] {
    _original: [ 'a', 'b', 'a' ],
    details: [ [Object] ]
  }
} 
[76]:
// Array with complex objects
const usersArraySchema = Joi.array().items(
Joi.object({
id: Joi.number().required(),
name: Joi.string().required(),
email: Joi.string().email().required()
})
).min(1);

// Test complex array validation
const users = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];

console.log("Complex array validation:\n");
console.log("Valid users array:", usersArraySchema.validate(users), "\n");
Complex array validation:
Valid users array: {
  value: [
    { id: 1, name: 'John', email: 'john@example.com' },
    { id: 2, name: 'Jane', email: 'jane@example.com' }
  ]
} 
[M]:

7. Conditional Validation

Explore advanced conditional validation techniques.

[77]:
// Basic conditional validation with when
const userRoleSchema = Joi.object({
role: Joi.string().valid('user', 'admin', 'editor').required(),
permissions: Joi.array().items(Joi.string())
.when('role', {
is: 'admin',
then: Joi.array().min(1).required(),
otherwise: Joi.optional()
})
});

// Test basic conditional validation
console.log("Basic conditional validation:\n");

const adminUser = {
role: 'admin',
permissions: ['read', 'write', 'delete']
};

console.log("Valid admin user:", userRoleSchema.validate(adminUser), "\n");

const invalidAdminUser = {
role: 'admin',
// Missing required permissions for admin
};

console.log("Invalid admin user (missing permissions):", userRoleSchema.validate(invalidAdminUser), "\n");

const regularUser = {
role: 'user'
// Permissions not required for regular user
};

console.log("Valid regular user (no permissions needed):", userRoleSchema.validate(regularUser), "\n");
Basic conditional validation:
Valid admin user: {
  value: { role: 'admin', permissions: [ 'read', 'write', 'delete' ] }
} 
Invalid admin user (missing permissions): {
  value: { role: 'admin' },
  error: [Error [ValidationError]: "permissions" is required] {
    _original: { role: 'admin' },
    details: [ [Object] ]
  }
} 
Valid regular user (no permissions needed): { value: { role: 'user' } } 
[78]:
// Advanced conditional validation with multiple conditions
const shippingSchema = Joi.object({
shippingMethod: Joi.string().valid('standard', 'express', 'international').required(),
// Domestic address (required for standard and express)
domesticAddress: Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
state: Joi.string().length(2).required(),
zipCode: Joi.string().pattern(/^\d{5}$/).required()
}).when('shippingMethod', {
is: Joi.valid('standard', 'express'),
then: Joi.required(),
otherwise: Joi.forbidden()
}),
// International address (required for international)
internationalAddress: Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
postalCode: Joi.string().required(),
country: Joi.string().required()
}).when('shippingMethod', {
is: 'international',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
// Express shipping requires a phone number
phoneNumber: Joi.string().pattern(/^\d{10}$/).when('shippingMethod', {
is: 'express',
then: Joi.required(),
otherwise: Joi.optional()
})
});
[79]:
// Test standard shipping
const standardShipping = {
shippingMethod: 'standard',
domesticAddress: {
street: '123 Main St',
city: 'Anytown',
state: 'CA',
zipCode: '12345'
}
};

console.log("Standard shipping validation:\n");
console.log(shippingSchema.validate(standardShipping), "\n");
Standard shipping validation:
{
  value: {
    shippingMethod: 'standard',
    domesticAddress: {
      street: '123 Main St',
      city: 'Anytown',
      state: 'CA',
      zipCode: '12345'
    }
  }
} 
[80]:
// Test express shipping
const expressShipping = {
shippingMethod: 'express',
domesticAddress: {
street: '123 Main St',
city: 'Anytown',
state: 'CA',
zipCode: '12345'
},
phoneNumber: '1234567890'
};

console.log("Express shipping validation:\n");
console.log(shippingSchema.validate(expressShipping), "\n");
Express shipping validation:
{
  value: {
    shippingMethod: 'express',
    domesticAddress: {
      street: '123 Main St',
      city: 'Anytown',
      state: 'CA',
      zipCode: '12345'
    },
    phoneNumber: '1234567890'
  }
} 
[81]:
// Test international shipping
const internationalShipping = {
shippingMethod: 'international',
internationalAddress: {
street: '123 Global Ave',
city: 'London',
postalCode: 'SW1A 1AA',
country: 'United Kingdom'
}
};

console.log("International shipping validation:\n");
console.log(shippingSchema.validate(internationalShipping), "\n");
International shipping validation:
{
  value: {
    shippingMethod: 'international',
    internationalAddress: {
      street: '123 Global Ave',
      city: 'London',
      postalCode: 'SW1A 1AA',
      country: 'United Kingdom'
    }
  }
} 
[82]:
// Test invalid shipping (mixing domestic and international)
const invalidShipping = {
shippingMethod: 'international',
domesticAddress: { // This should be forbidden for international
street: '123 Main St',
city: 'Anytown',
state: 'CA',
zipCode: '12345'
}
// Missing required internationalAddress
};

console.log("Invalid shipping validation:\n");
console.log(shippingSchema.validate(invalidShipping), "\n");
Invalid shipping validation:
{
  value: {
    shippingMethod: 'international',
    domesticAddress: {
      street: '123 Main St',
      city: 'Anytown',
      state: 'CA',
      zipCode: '12345'
    }
  },
  error: [Error [ValidationError]: "domesticAddress" is not allowed] {
    _original: { shippingMethod: 'international', domesticAddress: [Object] },
    details: [ [Object] ]
  }
} 
[M]:

8. Custom Validation Functions

Create custom validation logic for complex requirements.

[83]:
// Custom validation function for a product schema
const productSchema = Joi.object({
name: Joi.string().min(3).max(100).required(),
price: Joi.number().precision(2).positive().required(),
salePrice: Joi.number().precision(2).positive().optional(),
quantity: Joi.number().integer().min(0).required(),
category: Joi.string().required(),
tags: Joi.array().items(Joi.string()).optional()
});
[84]:
// Add custom validation to ensure salePrice is less than regular price
const productSchemaWithCustomValidation = productSchema.custom((value, helpers) => {
// Skip validation if salePrice is not provided
if (!value.salePrice) {
return value;
}
// Check if salePrice is less than regular price
if (value.salePrice >= value.price) {
return helpers.error('product.invalidSalePrice', {
price: value.price,
salePrice: value.salePrice
});
}
return value;
});
[85]:
// Add custom error message
const customErrors = {
'product.invalidSalePrice': '{{#label}} sale price ({{#salePrice}}) must be less than regular price ({{#price}})'
};

// Create a validation function with custom error messages
function validateProduct(product) {
return productSchemaWithCustomValidation.validate(product, {
errors: {
language: {
'product.invalidSalePrice': customErrors['product.invalidSalePrice']
}
}
});
}
[86]:
// Test valid product
const validProduct = {
name: 'Wireless Headphones',
price: 99.99,
salePrice: 79.99,
quantity: 50,
category: 'Electronics',
tags: ['wireless', 'audio', 'bluetooth']
};

console.log("Valid product validation:\n");
console.log(validateProduct(validProduct), "\n");
Valid product validation:
{
  value: {
    name: 'Wireless Headphones',
    price: 99.99,
    salePrice: 79.99,
    quantity: 50,
    category: 'Electronics',
    tags: [ 'wireless', 'audio', 'bluetooth' ]
  }
} 
[87]:
// Test invalid product (salePrice >= price)
const invalidProduct = {
name: 'Wireless Headphones',
price: 99.99,
salePrice: 109.99, // Higher than regular price
quantity: 50,
category: 'Electronics',
tags: ['wireless', 'audio', 'bluetooth']
};

console.log("Invalid product validation (sale price too high):\n");
console.log(validateProduct(invalidProduct), "\n");
Invalid product validation (sale price too high):
{
  value: {
    name: 'Wireless Headphones',
    price: 99.99,
    salePrice: 109.99,
    quantity: 50,
    category: 'Electronics',
    tags: [ 'wireless', 'audio', 'bluetooth' ]
  },
  error: [Error [ValidationError]: Error code "product.invalidSalePrice" is not defined, your custom type is missing the correct messages definition] {
    _original: {
      name: 'Wireless Headphones',
      price: 99.99,
      salePrice: 109.99,
      quantity: 50,
      category: 'Electronics',
      tags: [Array]
    },
    details: [ [Object] ]
  }
} 
[M]:

9. Schema Transformation and Defaults

Learn how to transform data and set default values during validation.

[88]:
// Schema with transformations and defaults
const blogPostSchema = Joi.object({
title: Joi.string().trim().min(5).max(100).required(),
slug: Joi.string().lowercase().optional(),
content: Joi.string().min(50).required(),
excerpt: Joi.string().max(200).optional(),
author: Joi.string().required(),
tags: Joi.array().items(Joi.string().lowercase()).default([]),
status: Joi.string().valid('draft', 'published', 'archived').default('draft'),
createdAt: Joi.date().default(Date.now),
views: Joi.number().integer().min(0).default(0),
featured: Joi.boolean().default(false)
});
[89]:
// Add transformation to generate slug from title if not provided
const blogPostSchemaWithTransform = blogPostSchema.custom((value, helpers) => {
// If slug is not provided, generate it from the title
if (!value.slug && value.title) {
// Simple slug generation (in real app, use a proper slugify library)
value.slug = value.title
.toLowerCase()
.replace(/[^\w\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-'); // Remove consecutive hyphens
}
// If excerpt is not provided, generate it from the content
if (!value.excerpt && value.content) {
// Take first 150 characters and add ellipsis
value.excerpt = value.content.substring(0, 150) +
(value.content.length > 150 ? '...' : '');
}
return value;
});
[90]:
// Test blog post with minimal data
const minimalPost = {
title: ' Introduction to Joi Schema Validation ', // Has extra spaces
content: 'This is a comprehensive guide to using Joi for schema validation in your Node.js applications. We will cover basic and advanced validation techniques.'.repeat(3), // Repeated to meet min length
author: 'John Doe'
// Missing optional fields and fields with defaults
};

const validatedPost = blogPostSchemaWithTransform.validate(minimalPost, { stripUnknown: true });
console.log("Blog post with transformations and defaults:\n");
console.log(validatedPost, "\n");

// Notice how the following were automatically handled:
// 1. title was trimmed
// 2. slug was generated from title
// 3. excerpt was generated from content
// 4. Default values were applied for tags, status, createdAt, views, and featured
Blog post with transformations and defaults:
{
  value: {
    title: 'Introduction to Joi Schema Validation',
    content: 'This is a comprehensive guide to using Joi for schema validation in your Node.js applications. We will cover basic and advanced validation techniques.This is a comprehensive guide to using Joi for schema validation in your Node.js applications. We will cover basic and advanced validation techniques.This is a comprehensive guide to using Joi for schema validation in your Node.js applications. We will cover basic and advanced validation techniques.',
    author: 'John Doe',
    tags: [],
    status: 'draft',
    createdAt: 1741806838238,
    views: 0,
    featured: false,
    slug: 'introduction-to-joi-schema-validation',
    excerpt: 'This is a comprehensive guide to using Joi for schema validation in your Node.js applications. We will cover basic and advanced validation techniques....'
  }
} 
[M]:

10. Schema Extensions and Plugins

Learn how to extend Joi with custom types and validation rules.

[91]:
// Create a custom extension for ISBN validation
const JoiWithIsbn = Joi.extend((joi) => ({
type: 'string',
base: joi.string(),
messages: {
'string.isbn10': '{{#label}} must be a valid ISBN-10',
'string.isbn13': '{{#label}} must be a valid ISBN-13'
},
rules: {
isbn10: {
validate(value, helpers) {
// Remove hyphens and spaces
const isbn = value.replace(/[\s-]/g, '');
// ISBN-10 must be 10 characters
if (isbn.length !== 10) {
return helpers.error('string.isbn10');
}
// Simple ISBN-10 check (in real app, use a proper validation algorithm)
const isValid = /^\d{9}[\dX]$/.test(isbn);
if (!isValid) {
return helpers.error('string.isbn10');
}
return value;
}
},
isbn13: {
validate(value, helpers) {
// Remove hyphens and spaces
const isbn = value.replace(/[\s-]/g, '');
// ISBN-13 must be 13 characters
if (isbn.length !== 13) {
[92]:
// Create a book schema using the custom ISBN extension
const bookSchema = JoiWithIsbn.object({
title: JoiWithIsbn.string().min(1).max(100).required(),
author: JoiWithIsbn.string().required(),
isbn10: JoiWithIsbn.string().isbn10().optional(),
isbn13: JoiWithIsbn.string().isbn13().optional(),
publisher: JoiWithIsbn.string().optional(),
publicationYear: JoiWithIsbn.number().integer().min(1800).max(new Date().getFullYear()).optional()
}).or('isbn10', 'isbn13'); // Require at least one ISBN
[93]:
// Test valid book with ISBN-10
const bookWithIsbn10 = {
title: 'JavaScript: The Good Parts',
author: 'Douglas Crockford',
isbn10: '0596517742',
publisher: 'O\'Reilly Media',
publicationYear: 2008
};

console.log("Book validation with ISBN-10:\n");
console.log(bookSchema.validate(bookWithIsbn10), "\n");
Book validation with ISBN-10:
{
  value: {
    title: 'JavaScript: The Good Parts',
    author: 'Douglas Crockford',
    isbn10: '0596517742',
    publisher: "O'Reilly Media",
    publicationYear: 2008
  }
} 
[94]:
// Test valid book with ISBN-13
const bookWithIsbn13 = {
title: 'Eloquent JavaScript',
author: 'Marijn Haverbeke',
isbn13: '9781593279509',
publisher: 'No Starch Press',
publicationYear: 2018
};

console.log("Book validation with ISBN-13:\n");
console.log(bookSchema.validate(bookWithIsbn13), "\n");
Book validation with ISBN-13:
{
  value: {
    title: 'Eloquent JavaScript',
    author: 'Marijn Haverbeke',
    isbn13: '9781593279509',
    publisher: 'No Starch Press',
    publicationYear: 2018
  }
} 
[95]:
// Test invalid book (no ISBN)
const bookWithoutIsbn = {
title: 'Some Book',
author: 'Some Author',
publisher: 'Some Publisher',
publicationYear: 2020
};

console.log("Book validation without ISBN:\n");
console.log(bookSchema.validate(bookWithoutIsbn), "\n");
Book validation without ISBN:
{
  value: {
    title: 'Some Book',
    author: 'Some Author',
    publisher: 'Some Publisher',
    publicationYear: 2020
  },
  error: [Error [ValidationError]: "value" must contain at least one of [isbn10, isbn13]] {
    _original: {
      title: 'Some Book',
      author: 'Some Author',
      publisher: 'Some Publisher',
      publicationYear: 2020
    },
    details: [ [Object] ]
  }
} 
[96]:
// Test invalid ISBN format
const bookWithInvalidIsbn = {
title: 'Invalid ISBN Book',
author: 'Test Author',
isbn10: '123456789', // Too short
publisher: 'Test Publisher',
publicationYear: 2021
};

console.log("Book validation with invalid ISBN:\n");
console.log(bookSchema.validate(bookWithInvalidIsbn), "\n");
Book validation with invalid ISBN:
{
  value: {
    title: 'Invalid ISBN Book',
    author: 'Test Author',
    isbn10: '123456789',
    publisher: 'Test Publisher',
    publicationYear: 2021
  },
  error: [Error [ValidationError]: "isbn10" must be a valid ISBN-10] {
    _original: {
      title: 'Invalid ISBN Book',
      author: 'Test Author',
      isbn10: '123456789',
      publisher: 'Test Publisher',
      publicationYear: 2021
    },
    details: [ [Object] ]
  }
} 
[M]:

Summary

This notebook has provided a comprehensive guide to building and validating schemas with Joi. We've covered:

  1. Schema Building Fundamentals: Core components and structure of Joi schemas
  2. Schema Composition and Reuse: Creating modular, reusable schema components
  3. Advanced String Validation: Complex string validation for URLs, emails, and patterns
  4. Object Schema Techniques: Nested objects, unknown keys, and schema references
  5. Alternatives and Conditional Logic: Using alternatives and conditional validation
  6. Array Schema Advanced Techniques: Validating arrays with various constraints
  7. Conditional Validation: Creating schemas with complex conditional logic
  8. Custom Validation Functions: Implementing custom validation rules
  9. Schema Transformation and Defaults: Transforming data and setting defaults
  10. Schema Extensions and Plugins: Extending Joi with custom types and rules

Joi provides a powerful, flexible system for data validation in JavaScript applications. By leveraging these techniques, you can ensure data integrity, improve error handling, and create more robust applications.

For more information, check out the Joi documentation.

Sign in to save your work and access it from anywhere