Logo
⚠️ Unsaved
[M]:

Joi 101: Getting Started with Schema Validation

This notebook introduces Joi, a powerful schema validation library for JavaScript. We'll explore how to use Joi to validate objects, forms, API payloads, and configuration data with simple, readable validation rules.

[ ]:
// Install Joi using npm
!npm install joi
[ ]:
// Import Joi
const Joi = require('joi');

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

1. Basic Validation

Let's start with some basic validation examples to understand how Joi works. Joi allows you to define schemas that describe the shape of your data.

[ ]:
// Define a simple schema for a user object
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
birthYear: Joi.number().integer().min(1900).max(2023),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),
});

// Valid user data
const validUser = {
username: 'johndoe',
email: 'john@example.com',
birthYear: 1990,
password: 'password123'
};

// Validate the user data against the schema
const validResult = userSchema.validate(validUser);
console.log("Valid user validation result:\n");
console.log(validResult, "\n");

// Invalid user data
const invalidUser = {
username: 'j', // too short
email: 'not-an-email',
birthYear: 1800, // too early
password: 'pw'
};

// Validate the invalid user data
const invalidResult = userSchema.validate(invalidUser);
console.log("Invalid user validation result:\n");
console.log(invalidResult, "\n");
[M]:

2. Validation Options

Joi provides several options to customize the validation behavior.

[ ]:
// Define a schema with some optional fields
const profileSchema = Joi.object({
name: Joi.string().required(),
age: Joi.number().integer().min(0),
email: Joi.string().email(),
website: Joi.string().uri(),
address: Joi.object({
street: Joi.string(),
city: Joi.string(),
zipCode: Joi.string()
})
});

// Data with unknown field
const profileData = {
name: 'Jane Smith',
age: 28,
email: 'jane@example.com',
favoriteColor: 'blue', // This field is not in the schema
address: {
street: '123 Main St',
city: 'Anytown',
zipCode: '12345',
country: 'USA' // This nested field is not in the schema
}
};

// By default, Joi will reject unknown fields
const strictResult = profileSchema.validate(profileData);
console.log("Strict validation (default):\n");
console.log(strictResult, "\n");

// Allow unknown fields
const allowUnknownResult = profileSchema.validate(profileData, { allowUnknown: true });
console.log("Validation with allowUnknown: true:\n");
console.log(allowUnknownResult, "\n");
[M]:

3. Advanced Validation Rules

Joi offers a rich set of validation rules for different data types.

[ ]:
// String validation
const stringSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
website: Joi.string().uri(),
hexColor: Joi.string().pattern(/^#[0-9A-F]{6}$/i),
ipAddress: Joi.string().ip(),
guid: Joi.string().guid(),
password: Joi.string().min(8).required(),
confirmPassword: Joi.string().valid(Joi.ref('password')).required().messages({
'any.only': 'Passwords must match'
})
});

// Test string validation
const stringData = {
username: 'johndoe',
email: 'john@example.com',
website: 'https://example.com',
hexColor: '#FF5733',
ipAddress: '192.168.1.1',
guid: '123e4567-e89b-12d3-a456-426614174000',
password: 'password123',
confirmPassword: 'password123'
};

const stringResult = stringSchema.validate(stringData);
console.log("String validation result:\n");
console.log(stringResult, "\n");

// Test password mismatch
const mismatchData = {
...stringData,
confirmPassword: 'different'
};

[ ]:
// Number validation
const numberSchema = Joi.object({
age: Joi.number().integer().min(0).max(120),
price: Joi.number().precision(2).positive(),
quantity: Joi.number().integer().positive(),
rating: Joi.number().min(1).max(5).precision(1),
percentage: Joi.number().min(0).max(100),
port: Joi.number().port()
});

// Test number validation
const numberData = {
age: 25,
price: 19.99,
quantity: 5,
rating: 4.5,
percentage: 75,
port: 8080
};

const numberResult = numberSchema.validate(numberData);
console.log("Number validation result:\n");
console.log(numberResult, "\n");

// Invalid number data
const invalidNumberData = {
age: -5,
price: -10.99,
quantity: 0,
rating: 6,
percentage: 110,
port: 70000
};

const invalidNumberResult = numberSchema.validate(invalidNumberData);
console.log("Invalid number validation result:\n");
[ ]:
// Date validation
const dateSchema = Joi.object({
birthdate: Joi.date().iso(),
registrationDate: Joi.date().greater('2020-01-01'),
appointmentDate: Joi.date().greater('now'),
expiryDate: Joi.date().less('2030-01-01'),
lastLogin: Joi.date().max('now')
});

// Test date validation
const dateData = {
birthdate: '1990-05-15',
registrationDate: '2021-03-10',
appointmentDate: new Date(Date.now() + 86400000), // tomorrow
expiryDate: '2025-12-31',
lastLogin: new Date()
};

const dateResult = dateSchema.validate(dateData);
console.log("Date validation result:\n");
console.log(dateResult, "\n");
[M]:

4. Array and Object Validation

Joi can validate arrays and nested objects with complex structures.

[ ]:
// Array validation
const arraySchema = Joi.object({
tags: Joi.array().items(Joi.string()).min(1).unique(),
scores: Joi.array().items(Joi.number().min(0).max(100)),
coordinates: Joi.array().ordered(
Joi.number(), // x
Joi.number() // y
),
matrix: Joi.array().items(Joi.array().items(Joi.number()))
});

// Test array validation
const arrayData = {
tags: ['javascript', 'validation', 'joi'],
scores: [85, 92, 78, 95],
coordinates: [10, 20],
matrix: [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
};

const arrayResult = arraySchema.validate(arrayData);
console.log("Array validation result:\n");
console.log(arrayResult, "\n");

// Invalid array data
const invalidArrayData = {
tags: ['javascript', 'javascript'], // duplicate
scores: [85, 110], // 110 > 100
coordinates: [10, 20, 30], // too many elements
matrix: [
[1, 2, 3],
[4, '5', 6], // string instead of number
[7, 8, 9]
[ ]:
// Nested object validation
const productSchema = Joi.object({
id: Joi.string().required(),
name: Joi.string().required(),
price: Joi.number().positive().precision(2).required(),
category: Joi.string().required(),
details: Joi.object({
description: Joi.string().max(500),
weight: Joi.number().positive(),
dimensions: Joi.object({
length: Joi.number().positive(),
width: Joi.number().positive(),
height: Joi.number().positive()
}),
manufacturer: Joi.object({
name: Joi.string().required(),
country: Joi.string(),
established: Joi.number().integer()
})
}),
inStock: Joi.boolean().default(true),
tags: Joi.array().items(Joi.string())
});

// Test nested object validation
const productData = {
id: 'prod-123',
name: 'Wireless Headphones',
price: 99.99,
category: 'Electronics',
details: {
description: 'High-quality wireless headphones with noise cancellation.',
weight: 0.3,
dimensions: {
length: 18,
width: 15,
[M]:

5. Conditional Validation

Joi allows for conditional validation based on the values of other fields.

[ ]:
// Conditional validation
const userRegistrationSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().min(8).required(),
email: Joi.string().email().required(),
accountType: Joi.string().valid('personal', 'business').required(),
// Company name is required only for business accounts
companyName: Joi.when('accountType', {
is: 'business',
then: Joi.string().required(),
otherwise: Joi.string().optional()
}),
// Tax ID is required only for business accounts
taxId: Joi.when('accountType', {
is: 'business',
then: Joi.string().pattern(/^\d{2}-\d{7}$/).required(),
otherwise: Joi.forbidden()
}),
// Age verification is required for personal accounts
age: Joi.when('accountType', {
is: 'personal',
then: Joi.number().integer().min(18).required(),
otherwise: Joi.optional()
}),
// Newsletter subscription is optional
newsletter: Joi.boolean().default(false),
// If subscribed to newsletter, frequency is required
newsletterFrequency: Joi.when('newsletter', {
is: true,
then: Joi.string().valid('daily', 'weekly', 'monthly').required(),
otherwise: Joi.forbidden()
})
});

// Test personal account
const personalAccount = {
[M]:

6. Custom Validation Messages

Joi allows you to customize error messages for better user experience.

[ ]:
// Custom validation messages
const signupSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required().messages({
'string.base': 'Username must be a text value',
'string.empty': 'Username cannot be empty',
'string.min': 'Username must be at least {#limit} characters long',
'string.max': 'Username cannot exceed {#limit} characters',
'string.alphanum': 'Username must only contain alphanumeric characters',
'any.required': 'Username is required'
}),
email: Joi.string().email().required().messages({
'string.email': 'Please enter a valid email address',
'any.required': 'Email address is required'
}),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required().messages({
'string.min': 'Password must be at least {#limit} characters long',
'string.pattern.base': 'Password must contain at least one uppercase letter, one lowercase letter, and one number',
'any.required': 'Password is required'
}),
confirmPassword: Joi.string().valid(Joi.ref('password')).required().messages({
'any.only': 'Passwords do not match',
'any.required': 'Please confirm your password'
}),
birthYear: Joi.number().integer().min(1900).max(new Date().getFullYear() - 18).messages({
'number.min': 'Birth year cannot be earlier than {#limit}',
'number.max': 'You must be at least 18 years old to register',
'number.base': 'Birth year must be a number',
'number.integer': 'Birth year must be a whole number'
})
});

// Test with invalid data to see custom messages
const invalidSignupData = {
username: 'a',
email: 'not-an-email',
password: 'weak',
[M]:

7. Practical Example: API Request Validation

Let's see how Joi can be used to validate API requests in a real-world scenario.

[ ]:
// API request validation example
function validateRequest(req) {
// Define schemas for different parts of the request
const schemas = {
// Schema for query parameters
query: Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(10),
sort: Joi.string().valid('asc', 'desc').default('asc'),
filter: Joi.string().optional()
}),
// Schema for request body (for POST/PUT requests)
body: Joi.object({
title: Joi.string().min(3).max(100).required(),
content: Joi.string().min(10).required(),
tags: Joi.array().items(Joi.string()).min(1).max(5),
published: Joi.boolean().default(false),
authorId: Joi.string().required()
}),
// Schema for URL parameters
params: Joi.object({
id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).required() // MongoDB ObjectId format
})
};
// Validate each part of the request
const result = {};
for (const key of Object.keys(req)) {
if (schemas[key]) {
const { error, value } = schemas[key].validate(req[key], { abortEarly: false });
if (error) {
result.error = error;
break;
[M]:

8. Environment Configuration Validation

Joi is great for validating environment variables and configuration objects.

[ ]:
// Environment configuration validation
const envSchema = Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
PORT: Joi.number().port().default(3000),
DATABASE_URL: Joi.string().uri().required(),
API_KEY: Joi.string().required(),
LOG_LEVEL: Joi.string().valid('error', 'warn', 'info', 'debug').default('info'),
ENABLE_CACHE: Joi.boolean().default(true),
CACHE_TTL: Joi.number().integer().min(0).default(300),
CORS_ORIGINS: Joi.alternatives().try(
Joi.string().valid('*'),
Joi.array().items(Joi.string().domain())
).default('*')
}).unknown();

// Sample environment variables
const env = {
NODE_ENV: 'production',
PORT: 8080,
DATABASE_URL: 'mongodb://localhost:27017/myapp',
API_KEY: 'secret-api-key-123',
LOG_LEVEL: 'warn',
ENABLE_CACHE: 'true', // String instead of boolean
CORS_ORIGINS: ['example.com', 'api.example.com']
// Missing CACHE_TTL, will use default
};

// Validate and convert environment variables
const { error, value: validatedEnv } = envSchema.validate(env, {
convert: true, // Convert string values to appropriate types
stripUnknown: false // Keep unknown environment variables
});

if (error) {
console.log("Environment validation error:\n");
console.log(error.details, "\n");
[M]:

9. Custom Validation Functions

Joi allows you to create custom validation functions for complex validation logic.

[ ]:
// Custom validation functions
const orderSchema = Joi.object({
customerId: Joi.string().required(),
items: Joi.array().items(
Joi.object({
productId: Joi.string().required(),
quantity: Joi.number().integer().min(1).required(),
price: Joi.number().precision(2).positive().required()
})
).min(1).required(),
couponCode: Joi.string().optional(),
shippingAddress: Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
state: Joi.string().length(2).required(),
zipCode: Joi.string().pattern(/^\d{5}(-\d{4})?$/).required()
}).required(),
billingAddress: Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
state: Joi.string().length(2).required(),
zipCode: Joi.string().pattern(/^\d{5}(-\d{4})?$/).required()
}).optional(),
paymentMethod: Joi.string().valid('credit_card', 'paypal', 'bank_transfer').required(),
creditCardInfo: Joi.object({
cardNumber: Joi.string().creditCard().required(),
expiryMonth: Joi.number().integer().min(1).max(12).required(),
expiryYear: Joi.number().integer().min(new Date().getFullYear()).required(),
cvv: Joi.string().pattern(/^\d{3,4}$/).required()
}).when('paymentMethod', {
is: 'credit_card',
then: Joi.required(),
otherwise: Joi.forbidden()
}),
totalAmount: Joi.number().precision(2).positive().required()
}).custom((value, helpers) => {
[M]:

10. Performance Considerations

Let's look at some performance considerations when using Joi.

[ ]:
// Performance test
const simpleSchema = Joi.object({
id: Joi.string().required(),
name: Joi.string().required(),
email: Joi.string().email().required()
});

const complexSchema = Joi.object({
id: Joi.string().required(),
name: Joi.string().min(3).max(50).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(18).max(100).required(),
address: Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
state: Joi.string().length(2).required(),
zipCode: Joi.string().pattern(/^\d{5}(-\d{4})?$/).required()
}).required(),
tags: Joi.array().items(Joi.string()).min(1).max(10).required(),
settings: Joi.object().pattern(Joi.string(), Joi.any()).required()
});

// Create test data
const simpleData = {
id: '123',
name: 'John Doe',
email: 'john@example.com'
};

const complexData = {
id: '123',
name: 'John Doe',
email: 'john@example.com',
age: 30,
address: {
street: '123 Main St',
[M]:

Summary

This notebook has introduced Joi, a powerful schema validation library for JavaScript. We've covered:

  1. Basic Validation: Creating schemas and validating data against them
  2. Validation Options: Customizing validation behavior with options like allowUnknown and stripUnknown
  3. Advanced Validation Rules: Using Joi's rich set of validation rules for different data types
  4. Conditional Validation: Creating schemas with conditional validation logic
  5. Complex Object Validation: Validating nested objects and arrays
  6. Custom Validation Messages: Customizing error messages for better user experience
  7. API Request Validation: Using Joi to validate API requests
  8. Environment Configuration Validation: Validating environment variables and configuration objects
  9. Custom Validation Functions: Creating custom validation logic
  10. Performance Considerations: Understanding performance implications

Joi is a versatile tool that can help ensure data integrity in your applications. It's particularly useful for:

  • Validating user input in forms
  • Validating API requests and responses
  • Validating configuration objects
  • Ensuring data consistency in your application

For more information, check out the Joi documentation.

Sign in to save your work and access it from anywhere