0
# Extensions and Customization
1
2
Extension system for creating custom schema types, validation rules, and modifying default behavior.
3
4
## Capabilities
5
6
### Extend Function
7
8
Adds custom schema types and validation rules to joi instances.
9
10
```typescript { .api }
11
/**
12
* Adds custom schema types and validation rules
13
* @param extensions - One or more extension definitions
14
* @returns New joi instance with added extensions
15
*/
16
function extend(...extensions: Extension[]): Root;
17
18
interface Extension {
19
// Extension identification
20
type: string | RegExp; // Extension type name or pattern
21
base?: Schema; // Base schema to extend from
22
23
// Custom messages
24
messages?: Record<string, string>;
25
26
// Value processing hooks
27
coerce?: (value: any, helpers: CustomHelpers) => CoerceResult;
28
pre?: (value: any, helpers: CustomHelpers) => any;
29
30
// Validation rules
31
rules?: Record<string, RuleOptions>;
32
33
// Schema behavior overrides
34
overrides?: Record<string, any>;
35
36
// Schema rebuilding
37
rebuild?: (schema: Schema) => Schema;
38
39
// Manifest support
40
manifest?: ManifestOptions;
41
42
// Constructor arguments handling
43
args?: (schema: Schema, ...args: any[]) => Schema;
44
45
// Schema description modification
46
describe?: (description: SchemaDescription) => SchemaDescription;
47
48
// Internationalization
49
language?: Record<string, string>;
50
}
51
52
interface RuleOptions {
53
// Rule configuration
54
method?: (...args: any[]) => Schema;
55
validate?: (value: any, helpers: CustomHelpers, args: any) => any;
56
args?: (string | RuleArgOptions)[];
57
58
// Rule behavior
59
multi?: boolean; // Allow multiple rule applications
60
priority?: boolean; // Execute rule with priority
61
manifest?: boolean; // Include in manifest
62
63
// Rule conversion
64
convert?: boolean; // Enable value conversion
65
}
66
67
interface CustomHelpers {
68
// Error generation
69
error(code: string, local?: any): ErrorReport;
70
71
// Schema access
72
schema: Schema;
73
state: ValidationState;
74
prefs: ValidationOptions;
75
76
// Original value
77
original: any;
78
79
// Validation utilities
80
warn(code: string, local?: any): void;
81
message(messages: LanguageMessages, local?: any): string;
82
}
83
84
interface CoerceResult {
85
value?: any;
86
errors?: ErrorReport[];
87
}
88
```
89
90
**Usage Examples:**
91
92
```javascript
93
const Joi = require('joi');
94
95
// Simple extension for credit card validation
96
const creditCardExtension = {
97
type: 'creditCard',
98
base: Joi.string(),
99
messages: {
100
'creditCard.invalid': '{{#label}} must be a valid credit card number'
101
},
102
rules: {
103
luhn: {
104
validate(value, helpers) {
105
// Luhn algorithm implementation
106
const digits = value.replace(/\D/g, '');
107
let sum = 0;
108
let isEven = false;
109
110
for (let i = digits.length - 1; i >= 0; i--) {
111
let digit = parseInt(digits[i]);
112
113
if (isEven) {
114
digit *= 2;
115
if (digit > 9) {
116
digit -= 9;
117
}
118
}
119
120
sum += digit;
121
isEven = !isEven;
122
}
123
124
if (sum % 10 !== 0) {
125
return helpers.error('creditCard.invalid');
126
}
127
128
return value;
129
}
130
}
131
}
132
};
133
134
// Create extended joi instance
135
const extendedJoi = Joi.extend(creditCardExtension);
136
137
// Use the new credit card type
138
const schema = extendedJoi.creditCard().luhn();
139
const { error } = schema.validate('4532015112830366'); // Valid Visa number
140
```
141
142
### Advanced Extension Example
143
144
```javascript
145
// Email domain extension with custom rules
146
const emailDomainExtension = {
147
type: 'emailDomain',
148
base: Joi.string(),
149
messages: {
150
'emailDomain.blockedDomain': '{{#label}} domain {{#domain}} is not allowed',
151
'emailDomain.requiredDomain': '{{#label}} must use domain {{#domain}}'
152
},
153
coerce(value, helpers) {
154
if (typeof value === 'string') {
155
return { value: value.toLowerCase().trim() };
156
}
157
return { value };
158
},
159
rules: {
160
allowDomains: {
161
method(domains) {
162
return this.$_addRule({ name: 'allowDomains', args: { domains } });
163
},
164
args: [
165
{
166
name: 'domains',
167
assert: Joi.array().items(Joi.string()).min(1),
168
message: 'must be an array of domain strings'
169
}
170
],
171
validate(value, helpers, { domains }) {
172
const emailDomain = value.split('@')[1];
173
if (!domains.includes(emailDomain)) {
174
return helpers.error('emailDomain.requiredDomain', { domain: domains.join(', ') });
175
}
176
return value;
177
}
178
},
179
180
blockDomains: {
181
method(domains) {
182
return this.$_addRule({ name: 'blockDomains', args: { domains } });
183
},
184
args: [
185
{
186
name: 'domains',
187
assert: Joi.array().items(Joi.string()).min(1),
188
message: 'must be an array of domain strings'
189
}
190
],
191
validate(value, helpers, { domains }) {
192
const emailDomain = value.split('@')[1];
193
if (domains.includes(emailDomain)) {
194
return helpers.error('emailDomain.blockedDomain', { domain: emailDomain });
195
}
196
return value;
197
}
198
}
199
}
200
};
201
202
const customJoi = Joi.extend(emailDomainExtension);
203
204
// Use custom email domain validation
205
const emailSchema = customJoi.emailDomain()
206
.allowDomains(['company.com', 'partner.org'])
207
.blockDomains(['competitor.com']);
208
```
209
210
### Defaults Function
211
212
Creates a new joi instance with modified default schema behavior.
213
214
```typescript { .api }
215
/**
216
* Creates new joi instance with default schema modifiers
217
* @param modifier - Function that modifies default schemas
218
* @returns New joi instance with modified defaults
219
*/
220
function defaults(modifier: (schema: AnySchema) => AnySchema): Root;
221
```
222
223
**Usage Examples:**
224
225
```javascript
226
// Create joi instance with stricter defaults
227
const strictJoi = Joi.defaults((schema) => {
228
return schema.options({
229
abortEarly: false, // Collect all errors
230
allowUnknown: false, // Disallow unknown keys
231
stripUnknown: true // Strip unknown keys
232
});
233
});
234
235
// All schemas created with strictJoi will have these defaults
236
const schema = strictJoi.object({
237
name: strictJoi.string().required(),
238
age: strictJoi.number()
239
});
240
241
// Custom defaults for specific needs
242
const apiJoi = Joi.defaults((schema) => {
243
return schema
244
.options({ convert: false }) // Disable type conversion
245
.strict(); // Enable strict mode
246
});
247
```
248
249
### Extension Patterns
250
251
#### Multi-Type Extensions
252
253
```javascript
254
// Extension that applies to multiple schema types
255
const timestampExtension = {
256
type: /^(string|number)$/, // Apply to string and number types
257
rules: {
258
timestamp: {
259
method(format = 'unix') {
260
return this.$_addRule({ name: 'timestamp', args: { format } });
261
},
262
args: [
263
{
264
name: 'format',
265
assert: Joi.string().valid('unix', 'javascript'),
266
message: 'must be "unix" or "javascript"'
267
}
268
],
269
validate(value, helpers, { format }) {
270
const timestamp = format === 'unix' ? value * 1000 : value;
271
const date = new Date(timestamp);
272
273
if (isNaN(date.getTime())) {
274
return helpers.error('timestamp.invalid');
275
}
276
277
return value;
278
}
279
}
280
},
281
messages: {
282
'timestamp.invalid': '{{#label}} must be a valid timestamp'
283
}
284
};
285
```
286
287
#### Function-Based Extensions
288
289
```javascript
290
// Extension factory function
291
const createValidationExtension = (validatorName, validatorFn) => {
292
return {
293
type: 'any',
294
rules: {
295
[validatorName]: {
296
method(...args) {
297
return this.$_addRule({
298
name: validatorName,
299
args: { params: args }
300
});
301
},
302
validate(value, helpers, { params }) {
303
const isValid = validatorFn(value, ...params);
304
if (!isValid) {
305
return helpers.error(`${validatorName}.invalid`);
306
}
307
return value;
308
}
309
}
310
},
311
messages: {
312
[`${validatorName}.invalid`]: `{{#label}} failed ${validatorName} validation`
313
}
314
};
315
};
316
317
// Create custom validators
318
const divisibleByExtension = createValidationExtension(
319
'divisibleBy',
320
(value, divisor) => value % divisor === 0
321
);
322
323
const extendedJoi = Joi.extend(divisibleByExtension);
324
const schema = extendedJoi.number().divisibleBy(5);
325
```
326
327
### Extension Helpers
328
329
#### Custom Helper Utilities
330
331
```typescript { .api }
332
interface CustomHelpers {
333
/**
334
* Creates validation error
335
* @param code - Error code for message lookup
336
* @param local - Local context variables
337
* @returns ErrorReport object
338
*/
339
error(code: string, local?: any): ErrorReport;
340
341
/**
342
* Creates warning (non-fatal error)
343
* @param code - Warning code for message lookup
344
* @param local - Local context variables
345
*/
346
warn(code: string, local?: any): void;
347
348
/**
349
* Formats message with local context
350
* @param messages - Message templates
351
* @param local - Local context variables
352
* @returns Formatted message string
353
*/
354
message(messages: LanguageMessages, local?: any): string;
355
356
// Context properties
357
schema: Schema; // Current schema being validated
358
state: ValidationState; // Current validation state
359
prefs: ValidationOptions; // Current preferences
360
original: any; // Original input value
361
}
362
363
interface ValidationState {
364
key?: string; // Current validation key
365
path: (string | number)[]; // Path to current value
366
parent?: any; // Parent object
367
reference?: any; // Reference context
368
ancestors?: any[]; // Ancestor objects
369
}
370
```
371
372
**Usage Examples:**
373
374
```javascript
375
const advancedExtension = {
376
type: 'advanced',
377
base: Joi.any(),
378
rules: {
379
customValidation: {
380
validate(value, helpers) {
381
// Access validation context
382
const path = helpers.state.path.join('.');
383
const parent = helpers.state.parent;
384
const prefs = helpers.prefs;
385
386
// Custom validation logic
387
if (value === 'invalid') {
388
return helpers.error('advanced.invalid', {
389
path,
390
value
391
});
392
}
393
394
// Issue warning for suspicious values
395
if (value === 'suspicious') {
396
helpers.warn('advanced.suspicious', { value });
397
}
398
399
return value;
400
}
401
}
402
},
403
messages: {
404
'advanced.invalid': 'Value {{#value}} at path {{#path}} is invalid',
405
'advanced.suspicious': 'Value {{#value}} might be suspicious'
406
}
407
};
408
```
409
410
### Pre-Processing and Coercion
411
412
```javascript
413
const preprocessingExtension = {
414
type: 'preprocessed',
415
base: Joi.string(),
416
417
// Pre-process values before validation
418
pre(value, helpers) {
419
if (typeof value === 'string') {
420
// Normalize whitespace and case
421
return value.trim().toLowerCase();
422
}
423
return value;
424
},
425
426
// Coerce values during validation
427
coerce(value, helpers) {
428
if (typeof value === 'number') {
429
return { value: value.toString() };
430
}
431
432
if (Array.isArray(value)) {
433
return {
434
errors: [helpers.error('preprocessed.notString')]
435
};
436
}
437
438
return { value };
439
},
440
441
messages: {
442
'preprocessed.notString': '{{#label}} cannot be converted to string'
443
}
444
};
445
```
446
447
### Extension Composition
448
449
```javascript
450
// Combine multiple extensions
451
const compositeJoi = Joi
452
.extend(creditCardExtension)
453
.extend(emailDomainExtension)
454
.extend(timestampExtension);
455
456
// Use multiple custom types together
457
const complexSchema = compositeJoi.object({
458
email: compositeJoi.emailDomain().allowDomains(['trusted.com']),
459
creditCard: compositeJoi.creditCard().luhn(),
460
timestamp: compositeJoi.number().timestamp('unix')
461
});
462
```