Logo
⚠️ Unsaved
[M]:

Building Complex Schemas with Joi

This notebook explores advanced techniques for building complex validation schemas with Joi, the powerful schema validation library for JavaScript. We'll cover nested objects, conditional validation, custom validators, and more to help you implement robust data validation in your Node.js applications.

[22]:
// 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
[23]:
// 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. Nested Object Schemas

Complex data often involves nested objects. Let's see how to validate them effectively.

[24]:
// Define address schema as a reusable component
const addressSchema = Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
state: Joi.string().length(2).uppercase().required(),
zipCode: Joi.string().pattern(/^\d{5}(-\d{4})?$/).required()
});
[25]:
// Define contact info schema
const contactInfoSchema = Joi.object({
email: Joi.string().email().required(),
phone: Joi.string().pattern(/^\d{3}-\d{3}-\d{4}$/).required(),
alternatePhone: Joi.string().pattern(/^\d{3}-\d{3}-\d{4}$/).optional()
});
[26]:
// Define user schema with nested objects
const userSchema = Joi.object({
id: Joi.string().guid({ version: 'uuidv4' }),
username: Joi.string().alphanum().min(3).max(30).required(),
name: Joi.object({
first: Joi.string().min(1).max(50).required(),
middle: Joi.string().max(50).allow('').optional(),
last: Joi.string().min(1).max(50).required()
}).required(),
contact: contactInfoSchema.required(),
addresses: Joi.object({
home: addressSchema.required(),
work: addressSchema.optional(),
shipping: addressSchema.optional()
}).required(),
createdAt: Joi.date().default(Date.now)
});
[27]:
// Sample user data
const userData = {
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'johndoe',
name: {
first: 'John',
middle: '',
last: 'Doe'
},
contact: {
email: 'john.doe@example.com',
phone: '555-123-4567'
},
addresses: {
home: {
street: '123 Main St',
city: 'Anytown',
state: 'CA',
zipCode: '12345'
},
shipping: {
street: '456 Market St',
city: 'Somewhere',
state: 'NY',
zipCode: '67890'
}
}
};

// Validate the user data
const result = userSchema.validate(userData);
console.log("Nested object validation result:\n");
console.log(result, "\n");
Nested object validation result:
{
  value: {
    id: '123e4567-e89b-12d3-a456-426614174000',
    username: 'johndoe',
    name: { first: 'John', middle: '', last: 'Doe' },
    contact: { email: 'john.doe@example.com', phone: '555-123-4567' },
    addresses: { home: [Object], shipping: [Object] }
  },
  error: [Error [ValidationError]: "id" must be a valid GUID] {
    _original: {
      id: '123e4567-e89b-12d3-a456-426614174000',
      username: 'johndoe',
      name: [Object],
      contact: [Object],
      addresses: [Object]
    },
    details: [ [Object] ]
  }
} 
[M]:

2. Conditional Validation

Sometimes validation rules depend on other fields. Let's explore conditional validation.

[28]:
// Payment method schema with conditional validation
const paymentSchema = Joi.object({
method: Joi.string().valid('credit_card', 'paypal', 'bank_transfer').required(),
// Credit card details (required only for credit_card method)
cardNumber: Joi.string().creditCard().when('method', {
is: 'credit_card',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
expiryMonth: Joi.number().integer().min(1).max(12).when('method', {
is: 'credit_card',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
expiryYear: Joi.number().integer().min(new Date().getFullYear()).when('method', {
is: 'credit_card',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
cvv: Joi.string().pattern(/^\d{3,4}$/).when('method', {
is: 'credit_card',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
// PayPal details (required only for paypal method)
paypalEmail: Joi.string().email().when('method', {
is: 'paypal',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
// Bank transfer details (required only for bank_transfer method)
accountNumber: Joi.string().pattern(/^\d{10,12}$/).when('method', {
is: 'bank_transfer',
[29]:
// Test credit card payment
const creditCardPayment = {
method: 'credit_card',
cardNumber: '4111111111111111',
expiryMonth: 12,
expiryYear: new Date().getFullYear() + 1,
cvv: '123'
};

console.log("Credit card payment validation:\n");
console.log(paymentSchema.validate(creditCardPayment), "\n");
Credit card payment validation:
{
  value: {
    method: 'credit_card',
    cardNumber: '4111111111111111',
    expiryMonth: 12,
    expiryYear: 2026,
    cvv: '123'
  }
} 
[30]:
// Test PayPal payment
const paypalPayment = {
method: 'paypal',
paypalEmail: 'user@example.com'
};

console.log("PayPal payment validation:\n");
console.log(paymentSchema.validate(paypalPayment), "\n");
PayPal payment validation:
{ value: { method: 'paypal', paypalEmail: 'user@example.com' } } 
[31]:
// Test invalid payment (mixing methods)
const invalidPayment = {
method: 'credit_card',
paypalEmail: 'user@example.com' // Should be forbidden for credit_card
};

console.log("Invalid payment validation:\n");
console.log(paymentSchema.validate(invalidPayment), "\n");
Invalid payment validation:
{
  value: { method: 'credit_card', paypalEmail: 'user@example.com' },
  error: [Error [ValidationError]: "cardNumber" is required] {
    _original: { method: 'credit_card', paypalEmail: 'user@example.com' },
    details: [ [Object] ]
  }
} 
[M]:

3. Complex Array Validation

Let's explore validating arrays with complex items and constraints.

[32]:
// Product schema for order items
const productSchema = Joi.object({
id: Joi.string().required(),
name: Joi.string().required(),
price: Joi.number().positive().precision(2).required(),
quantity: Joi.number().integer().min(1).required(),
options: Joi.object().pattern(
Joi.string(), // Option name (key)
Joi.alternatives().try(
Joi.string(),
Joi.number(),
Joi.boolean()
)
).optional()
});
[33]:
// Order schema with array of products
const orderSchema = Joi.object({
orderId: Joi.string().guid({ version: 'uuidv4' }).required(),
customerId: Joi.string().required(),
items: Joi.array().items(productSchema).min(1).required(),
shippingAddress: addressSchema.required(),
billingAddress: addressSchema.default(Joi.ref('shippingAddress')),
paymentMethod: paymentSchema.required(),
orderDate: Joi.date().default(Date.now),
status: Joi.string().valid('pending', 'processing', 'shipped', 'delivered').default('pending')
}).custom((value, helpers) => {
// Custom validation to ensure total matches sum of items
const calculatedTotal = value.items.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0);
// Add calculated total to the order
value.total = parseFloat(calculatedTotal.toFixed(2));
return value;
});
[34]:
// Sample order data
const orderData = {
orderId: '123e4567-e89b-12d3-a456-426614174000',
customerId: 'cust-12345',
items: [
{
id: 'prod-1',
name: 'Laptop',
price: 999.99,
quantity: 1,
options: {
color: 'silver',
ram: 16,
ssd: 512,
extendedWarranty: true
}
},
{
id: 'prod-2',
name: 'Mouse',
price: 24.99,
quantity: 2
}
],
shippingAddress: {
street: '123 Main St',
city: 'Anytown',
state: 'CA',
zipCode: '12345'
},
paymentMethod: {
method: 'credit_card',
cardNumber: '4111111111111111',
expiryMonth: 12,
expiryYear: new Date().getFullYear() + 1,
cvv: '123'
Complex order validation result:
{
  value: {
    orderId: '123e4567-e89b-12d3-a456-426614174000',
    customerId: 'cust-12345',
    items: [ [Object], [Object] ],
    shippingAddress: {
      street: '123 Main St',
      city: 'Anytown',
      state: 'CA',
      zipCode: '12345'
    },
    paymentMethod: {
      method: 'credit_card',
      cardNumber: '4111111111111111',
      expiryMonth: 12,
      expiryYear: 2026,
      cvv: '123'
    }
  },
  error: [Error [ValidationError]: "orderId" must be a valid GUID] {
    _original: {
      orderId: '123e4567-e89b-12d3-a456-426614174000',
      customerId: 'cust-12345',
      items: [Array],
      shippingAddress: [Object],
      paymentMethod: [Object]
    },
    details: [ [Object] ]
  }
} 
Calculated total: undefined 
[M]:

4. Dynamic Keys with Pattern Validation

Sometimes you need to validate objects with dynamic keys.

[35]:
// Metadata schema with pattern validation for keys
const metadataSchema = Joi.object()
.pattern(
/^[a-z][a-z0-9_]*$/, // Key pattern: starts with letter, contains only letters, numbers, underscores
Joi.alternatives().try(
Joi.string(),
Joi.number(),
Joi.boolean(),
Joi.array().items(Joi.string())
)
)
.min(1) // At least one metadata field required
.max(20); // Maximum 20 metadata fields allowed
[36]:
// Content item with dynamic metadata
const contentSchema = Joi.object({
id: Joi.string().required(),
title: Joi.string().required(),
content: Joi.string().required(),
type: Joi.string().valid('article', 'video', 'podcast', 'image').required(),
metadata: metadataSchema.required()
});
[37]:
// Sample content with metadata
const articleData = {
id: 'article-123',
title: 'Understanding Joi Validation',
content: 'Joi is a powerful schema validation library...',
type: 'article',
metadata: {
author: 'John Doe',
published_date: '2023-05-15',
read_time: 5,
tags: ['javascript', 'validation', 'nodejs'],
featured: true,
category: 'programming'
}
};

console.log("Content with dynamic metadata validation:\n");
console.log(contentSchema.validate(articleData), "\n");
Content with dynamic metadata validation:
{
  value: {
    id: 'article-123',
    title: 'Understanding Joi Validation',
    content: 'Joi is a powerful schema validation library...',
    type: 'article',
    metadata: {
      author: 'John Doe',
      published_date: '2023-05-15',
      read_time: 5,
      tags: [Array],
      featured: true,
      category: 'programming'
    }
  }
} 
[38]:
// Invalid metadata (key starts with number)
const invalidMetadata = {
id: 'article-456',
title: 'Invalid Metadata Example',
content: 'This example has invalid metadata...',
type: 'article',
metadata: {
author: 'Jane Smith',
'1invalid_key': 'This key starts with a number', // Invalid key
tags: ['validation', 'example']
}
};

console.log("Invalid metadata validation:\n");
console.log(contentSchema.validate(invalidMetadata), "\n");
Invalid metadata validation:
{
  value: {
    id: 'article-456',
    title: 'Invalid Metadata Example',
    content: 'This example has invalid metadata...',
    type: 'article',
    metadata: {
      author: 'Jane Smith',
      '1invalid_key': 'This key starts with a number',
      tags: [Array]
    }
  },
  error: [Error [ValidationError]: "metadata.1invalid_key" is not allowed] {
    _original: {
      id: 'article-456',
      title: 'Invalid Metadata Example',
      content: 'This example has invalid metadata...',
      type: 'article',
      metadata: [Object]
    },
    details: [ [Object] ]
  }
} 
[M]:

5. Interdependent Fields

Let's validate fields that depend on each other.

[39]:
// Date range schema with interdependent validation
const dateRangeSchema = Joi.object({
startDate: Joi.date().iso().required(),
endDate: Joi.date().iso().min(Joi.ref('startDate')).required(),
duration: Joi.number().integer().positive().optional()
}).custom((value, helpers) => {
// Calculate and add duration in days if not provided
if (!value.duration) {
const start = new Date(value.startDate);
const end = new Date(value.endDate);
const diffTime = Math.abs(end - start);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
value.duration = diffDays;
} else {
// If duration is provided, verify it matches the date range
const start = new Date(value.startDate);
const end = new Date(value.endDate);
const diffTime = Math.abs(end - start);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (value.duration !== diffDays) {
return helpers.error('dateRange.durationMismatch', {
expected: diffDays,
provided: value.duration
});
}
}
return value;
});
[40]:
// Sample date range without duration
const dateRange1 = {
startDate: '2023-01-01',
endDate: '2023-01-05'
};

console.log("Date range validation (without duration):\n");
console.log(dateRangeSchema.validate(dateRange1), "\n");
Date range validation (without duration):
{
  value: {
    startDate: 2023-01-01T00:00:00.000Z,
    endDate: 2023-01-05T00:00:00.000Z,
    duration: 4
  }
} 
[41]:
// Sample date range with correct duration
const dateRange2 = {
startDate: '2023-02-10',
endDate: '2023-02-15',
duration: 5
};

console.log("Date range validation (with correct duration):\n");
console.log(dateRangeSchema.validate(dateRange2), "\n");
Date range validation (with correct duration):
{
  value: {
    startDate: 2023-02-10T00:00:00.000Z,
    endDate: 2023-02-15T00:00:00.000Z,
    duration: 5
  }
} 
[42]:
// Sample date range with incorrect duration
const dateRange3 = {
startDate: '2023-03-20',
endDate: '2023-03-25',
duration: 10 // Incorrect, should be 5
};

console.log("Date range validation (with incorrect duration):\n");
console.log(dateRangeSchema.validate(dateRange3), "\n");
Date range validation (with incorrect duration):
{
  value: {
    startDate: 2023-03-20T00:00:00.000Z,
    endDate: 2023-03-25T00:00:00.000Z,
    duration: 10
  },
  error: [Error [ValidationError]: Error code "dateRange.durationMismatch" is not defined, your custom type is missing the correct messages definition] {
    _original: { startDate: '2023-03-20', endDate: '2023-03-25', duration: 10 },
    details: [ [Object] ]
  }
} 
[M]:

Summary

This notebook has demonstrated advanced techniques for building complex schemas with Joi:

  1. Nested Object Schemas: Creating reusable schema components and composing them into complex structures
  2. Conditional Validation: Implementing validation rules that depend on other fields
  3. Complex Array Validation: Validating arrays with complex item schemas and constraints
  4. Dynamic Keys with Pattern Validation: Validating objects with dynamic keys that follow specific patterns
  5. Interdependent Fields: Validating fields that depend on each other's values

These techniques enable you to build robust validation for even the most complex data structures in your JavaScript applications.

For more information, check out the Joi documentation.

Sign in to save your work and access it from anywhere