or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

extensions-customization.mdindex.mdreferences-expressions.mdschema-types.mdutility-functions.mdvalidation-methods.md
tile.json

extensions-customization.mddocs/

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

```