joi 18.0.2 → 18.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/types/keys.js CHANGED
@@ -47,6 +47,70 @@ module.exports = Any.extend({
47
47
  return schema.keys(keys);
48
48
  },
49
49
 
50
+ jsonSchema(schema, res, mode, options) {
51
+
52
+ res.type = 'object';
53
+
54
+ // Map Joi keys to JSON Schema 'properties' and 'required'
55
+
56
+ if (schema.$_terms.keys) {
57
+ res.properties = {};
58
+
59
+ const required = [];
60
+
61
+ for (const child of schema.$_terms.keys) {
62
+ const jsonSchema = child.schema.$_jsonSchema(mode, options);
63
+ res.properties[child.key] = jsonSchema;
64
+ if (child.schema._flags.presence === 'required' ||
65
+ (mode === 'output' && child.schema._flags.default !== undefined)) {
66
+
67
+ required.push(child.key);
68
+ }
69
+ }
70
+
71
+ if (required.length) {
72
+ res.required = required.sort();
73
+ }
74
+ }
75
+
76
+ // Map Joi patterns to JSON Schema 'patternProperties' or 'additionalProperties'
77
+
78
+ if (schema.$_terms.patterns) {
79
+ const patternProperties = {};
80
+
81
+ for (const pattern of schema.$_terms.patterns) {
82
+ if (pattern.regex) {
83
+ patternProperties[pattern.regex.source] = pattern.rule.$_jsonSchema(mode, options);
84
+ }
85
+ else {
86
+ const isAny = pattern.schema.type === 'any';
87
+ if (isAny) {
88
+ res.additionalProperties = pattern.rule.$_jsonSchema(mode, options);
89
+ }
90
+ else {
91
+ // Best effort for schema-based patterns that are not 'any'
92
+ patternProperties['.*'] = pattern.rule.$_jsonSchema(mode, options);
93
+ }
94
+ }
95
+ }
96
+
97
+ if (Object.keys(patternProperties).length) {
98
+ res.patternProperties = patternProperties;
99
+ }
100
+ }
101
+
102
+ // Handle 'additionalProperties' based on unknown keys flag
103
+
104
+ if (res.additionalProperties === undefined) {
105
+ const additionalProperties = schema._flags.unknown === true || (schema._flags.unknown === undefined && !schema.$_terms.keys && !schema.$_terms.patterns && !schema._flags.only);
106
+ if (additionalProperties === false) {
107
+ res.additionalProperties = false;
108
+ }
109
+ }
110
+
111
+ return res;
112
+ },
113
+
50
114
  validate(value, { schema, error, state, prefs }) {
51
115
 
52
116
  if (!value ||
@@ -278,6 +342,12 @@ module.exports = Any.extend({
278
342
 
279
343
  return helpers.error('object.' + name, { limit: args.limit, value });
280
344
  },
345
+ jsonSchema(rule, res) {
346
+
347
+ res.minProperties = rule.args.limit;
348
+ res.maxProperties = rule.args.limit;
349
+ return res;
350
+ },
281
351
  args: [
282
352
  {
283
353
  name: 'limit',
@@ -292,6 +362,11 @@ module.exports = Any.extend({
292
362
  method(limit) {
293
363
 
294
364
  return this.$_addRule({ name: 'max', method: 'length', args: { limit }, operator: '<=' });
365
+ },
366
+ jsonSchema(rule, res) {
367
+
368
+ res.maxProperties = rule.args.limit;
369
+ return res;
295
370
  }
296
371
  },
297
372
 
@@ -299,6 +374,11 @@ module.exports = Any.extend({
299
374
  method(limit) {
300
375
 
301
376
  return this.$_addRule({ name: 'min', method: 'length', args: { limit }, operator: '>=' });
377
+ },
378
+ jsonSchema(rule, res) {
379
+
380
+ res.minProperties = rule.args.limit;
381
+ return res;
302
382
  }
303
383
  },
304
384
 
package/lib/types/link.js CHANGED
@@ -29,6 +29,29 @@ module.exports = Any.extend({
29
29
  return schema.ref(ref);
30
30
  },
31
31
 
32
+ jsonSchema(schema, res, mode, options) {
33
+
34
+ if (!schema.$_terms.link) {
35
+ return res;
36
+ }
37
+
38
+ const { ref } = schema.$_terms.link[0];
39
+
40
+ if (ref.ancestor === 'root' || ref.ancestor > 0) {
41
+ res.$ref = `#/${ref.path.map((p) => `properties/${p}`).join('/')}`;
42
+ return res;
43
+ }
44
+
45
+ if (ref.path.length === 1) {
46
+ res.$ref = `#/$defs/${ref.path[0]}`;
47
+ }
48
+ else {
49
+ res.$ref = `#/${ref.path.slice(1).map((p) => `properties/${p}`).join('/')}`;
50
+ }
51
+
52
+ return res;
53
+ },
54
+
32
55
  validate(value, { schema, state, prefs }) {
33
56
 
34
57
  assert(schema.$_terms.link, 'Uninitialized link schema');
@@ -136,6 +136,11 @@ module.exports = Any.extend({
136
136
  method(limit) {
137
137
 
138
138
  return this.$_addRule({ name: 'greater', method: 'compare', args: { limit }, operator: '>' });
139
+ },
140
+ jsonSchema(rule, res) {
141
+
142
+ res.exclusiveMinimum = rule.args.limit;
143
+ return res;
139
144
  }
140
145
  },
141
146
 
@@ -151,6 +156,12 @@ module.exports = Any.extend({
151
156
  }
152
157
 
153
158
  return helpers.error('number.integer');
159
+ },
160
+ jsonSchema(rule, res) {
161
+
162
+ res.type = 'integer';
163
+
164
+ return res;
154
165
  }
155
166
  },
156
167
 
@@ -158,6 +169,11 @@ module.exports = Any.extend({
158
169
  method(limit) {
159
170
 
160
171
  return this.$_addRule({ name: 'less', method: 'compare', args: { limit }, operator: '<' });
172
+ },
173
+ jsonSchema(rule, res) {
174
+
175
+ res.exclusiveMaximum = rule.args.limit;
176
+ return res;
161
177
  }
162
178
  },
163
179
 
@@ -165,6 +181,11 @@ module.exports = Any.extend({
165
181
  method(limit) {
166
182
 
167
183
  return this.$_addRule({ name: 'max', method: 'compare', args: { limit }, operator: '<=' });
184
+ },
185
+ jsonSchema(rule, res) {
186
+
187
+ res.maximum = rule.args.limit;
188
+ return res;
168
189
  }
169
190
  },
170
191
 
@@ -172,6 +193,11 @@ module.exports = Any.extend({
172
193
  method(limit) {
173
194
 
174
195
  return this.$_addRule({ name: 'min', method: 'compare', args: { limit }, operator: '>=' });
196
+ },
197
+ jsonSchema(rule, res) {
198
+
199
+ res.minimum = rule.args.limit;
200
+ return res;
175
201
  }
176
202
  },
177
203
 
@@ -203,6 +229,11 @@ module.exports = Any.extend({
203
229
  value :
204
230
  helpers.error('number.multiple', { multiple: options.args.base, value });
205
231
  },
232
+ jsonSchema(rule, res) {
233
+
234
+ res.multipleOf = rule.args.base;
235
+ return res;
236
+ },
206
237
  args: [
207
238
  {
208
239
  name: 'base',
@@ -284,6 +315,11 @@ module.exports = Any.extend({
284
315
  }
285
316
 
286
317
  return helpers.error(`number.${sign}`);
318
+ },
319
+ jsonSchema(rule, res) {
320
+
321
+ res['x-constraint'] = { ...res['x-constraint'], sign: rule.args.sign };
322
+ return res;
287
323
  }
288
324
  },
289
325
 
@@ -144,6 +144,23 @@ module.exports = Any.extend({
144
144
  }
145
145
  },
146
146
 
147
+ jsonSchema(schema, res, mode, options) {
148
+
149
+ const noEmpty = !schema._valids?.has('') && !schema._flags.only;
150
+ if (noEmpty) {
151
+ const min = schema.$_getRule('min');
152
+ const length = schema.$_getRule('length');
153
+
154
+ if ((!min || min.args.limit > 0) &&
155
+ (!length || length.args.limit > 0)) {
156
+
157
+ res.minLength = 1;
158
+ }
159
+ }
160
+
161
+ return res;
162
+ },
163
+
147
164
  rules: {
148
165
 
149
166
  alphanum: {
@@ -180,6 +197,11 @@ module.exports = Any.extend({
180
197
  }
181
198
 
182
199
  return helpers.error('string.base64');
200
+ },
201
+ jsonSchema(rule, res) {
202
+
203
+ res.format = 'base64';
204
+ return res;
183
205
  }
184
206
  },
185
207
 
@@ -260,6 +282,11 @@ module.exports = Any.extend({
260
282
  }
261
283
 
262
284
  return helpers.error('string.dataUri');
285
+ },
286
+ jsonSchema(rule, res) {
287
+
288
+ res.format = 'data-uri';
289
+ return res;
263
290
  }
264
291
  },
265
292
 
@@ -309,6 +336,11 @@ module.exports = Any.extend({
309
336
  }
310
337
 
311
338
  return helpers.error('string.email', { value, invalids });
339
+ },
340
+ jsonSchema(rule, res) {
341
+
342
+ res.format = 'email';
343
+ return res;
312
344
  }
313
345
  },
314
346
 
@@ -398,6 +430,11 @@ module.exports = Any.extend({
398
430
  }
399
431
 
400
432
  return value;
433
+ },
434
+ jsonSchema(rule, res) {
435
+
436
+ res.format = 'uuid';
437
+ return res;
401
438
  }
402
439
  },
403
440
 
@@ -430,6 +467,11 @@ module.exports = Any.extend({
430
467
  }
431
468
 
432
469
  return value;
470
+ },
471
+ jsonSchema(rule, res) {
472
+
473
+ res.format = 'hex';
474
+ return res;
433
475
  }
434
476
  },
435
477
 
@@ -447,6 +489,11 @@ module.exports = Any.extend({
447
489
  }
448
490
 
449
491
  return helpers.error('string.hostname');
492
+ },
493
+ jsonSchema(rule, res) {
494
+
495
+ res.format = 'hostname';
496
+ return res;
450
497
  }
451
498
  },
452
499
 
@@ -477,6 +524,18 @@ module.exports = Any.extend({
477
524
  }
478
525
 
479
526
  return helpers.error('string.ip', { value, cidr: options.cidr });
527
+ },
528
+ jsonSchema(rule, res) {
529
+
530
+ const version = rule.args.options.version;
531
+ if (version && version.length === 1) {
532
+ res.format = version[0];
533
+ }
534
+ else {
535
+ res.format = 'ip';
536
+ }
537
+
538
+ return res;
480
539
  }
481
540
  },
482
541
 
@@ -492,6 +551,11 @@ module.exports = Any.extend({
492
551
  }
493
552
 
494
553
  return error('string.isoDate');
554
+ },
555
+ jsonSchema(rule, res) {
556
+
557
+ res.format = 'date-time';
558
+ return res;
495
559
  }
496
560
  },
497
561
 
@@ -507,6 +571,11 @@ module.exports = Any.extend({
507
571
  }
508
572
 
509
573
  return helpers.error('string.isoDuration');
574
+ },
575
+ jsonSchema(rule, res) {
576
+
577
+ res.format = 'duration';
578
+ return res;
510
579
  }
511
580
  },
512
581
 
@@ -524,6 +593,12 @@ module.exports = Any.extend({
524
593
 
525
594
  return helpers.error('string.' + name, { limit: args.limit, value, encoding });
526
595
  },
596
+ jsonSchema(rule, res) {
597
+
598
+ res.minLength = rule.args.limit;
599
+ res.maxLength = rule.args.limit;
600
+ return res;
601
+ },
527
602
  args: [
528
603
  {
529
604
  name: 'limit',
@@ -547,6 +622,11 @@ module.exports = Any.extend({
547
622
 
548
623
  return internals.length(this, 'max', limit, '<=', encoding);
549
624
  },
625
+ jsonSchema(rule, res) {
626
+
627
+ res.maxLength = rule.args.limit;
628
+ return res;
629
+ },
550
630
  args: ['limit', 'encoding']
551
631
  },
552
632
 
@@ -555,6 +635,15 @@ module.exports = Any.extend({
555
635
 
556
636
  return internals.length(this, 'min', limit, '>=', encoding);
557
637
  },
638
+ jsonSchema(rule, res) {
639
+
640
+ if (rule.args.limit > 0) {
641
+
642
+ res.minLength = rule.args.limit;
643
+ }
644
+
645
+ return res;
646
+ },
558
647
  args: ['limit', 'encoding']
559
648
  },
560
649
 
@@ -602,6 +691,11 @@ module.exports = Any.extend({
602
691
 
603
692
  return helpers.error(errorCode, { name: options.name, regex, value });
604
693
  },
694
+ jsonSchema(rule, res) {
695
+
696
+ res.pattern = rule.args.regex.source;
697
+ return res;
698
+ },
605
699
  args: ['regex', 'options'],
606
700
  multi: true
607
701
  },
@@ -639,6 +733,11 @@ module.exports = Any.extend({
639
733
  }
640
734
 
641
735
  return helpers.error('string.token');
736
+ },
737
+ jsonSchema(rule, res) {
738
+
739
+ res.format = 'token';
740
+ return res;
642
741
  }
643
742
  },
644
743
 
@@ -728,6 +827,11 @@ module.exports = Any.extend({
728
827
  }
729
828
 
730
829
  return helpers.error('string.uri');
830
+ },
831
+ jsonSchema(rule, res) {
832
+
833
+ res.format = 'uri';
834
+ return res;
731
835
  }
732
836
  }
733
837
  },
@@ -95,6 +95,18 @@ module.exports = Any.extend({
95
95
  }
96
96
  },
97
97
 
98
+ jsonSchema(schema, json, mode, options) {
99
+
100
+ const map = schema.$_terms.map;
101
+ if (!map.size) {
102
+ return {};
103
+ }
104
+
105
+ return {
106
+ anyOf: Array.from(map.keys()).map((key) => ({ const: key }))
107
+ };
108
+ },
109
+
98
110
  messages: {
99
111
  'symbol.base': '{{#label}} must be a symbol',
100
112
  'symbol.map': '{{#label}} must be one of {{#map}}'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "joi",
3
3
  "description": "Object schema validation",
4
- "version": "18.0.2",
4
+ "version": "18.1.0",
5
5
  "repository": {
6
6
  "url": "git://github.com/hapijs/joi",
7
7
  "type": "git"
@@ -27,7 +27,7 @@
27
27
  "@hapi/pinpoint": "^2.0.1",
28
28
  "@hapi/tlds": "^1.1.1",
29
29
  "@hapi/topo": "^6.0.2",
30
- "@standard-schema/spec": "^1.0.0"
30
+ "@standard-schema/spec": "^1.1.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@hapi/bourne": "^3.0.0",
@@ -36,6 +36,7 @@
36
36
  "@hapi/joi-legacy-test": "npm:@hapi/joi@15.x.x",
37
37
  "@hapi/lab": "^26.0.0",
38
38
  "@types/node": "^20.17.47",
39
+ "ajv": "^8.18.0",
39
40
  "typescript": "^5.8.3"
40
41
  },
41
42
  "scripts": {