@bedrockio/yada 1.9.0 → 1.10.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/CHANGELOG.md CHANGED
@@ -1,10 +1,12 @@
1
- ## 1.8.2
2
-
3
- - Fixed array schemas not passing through `requireAllWithin`.
1
+ ## 1.10.0
4
2
 
5
- ## 1.8.1
6
-
7
- - Fixed bug with tagging in `toOpenApi`.
3
+ - Added `style` option to `toJsonSchema` to control disallowed formats for
4
+ OpenAI.
5
+ - Now using `integer` as the type for integers in JSON schema.
6
+ - Allow stripping out JSON schema extensions by option.
7
+ - More strictly following JSON schema for nullable.
8
+ - Allow flags to flip off required and nullable.
9
+ - Moved `requireAllWithin` to `transform`.
8
10
 
9
11
  ## 1.9.0
10
12
 
@@ -14,6 +16,14 @@
14
16
 
15
17
  - Fixed issue with incorrect JSON schema when nested.
16
18
 
19
+ ## 1.8.2
20
+
21
+ - Fixed array schemas not passing through `requireAllWithin`.
22
+
23
+ ## 1.8.1
24
+
25
+ - Fixed bug with tagging in `toOpenApi`.
26
+
17
27
  ## 1.8.0
18
28
 
19
29
  - Removed undefined `format` field.
@@ -7,6 +7,7 @@ exports.default = void 0;
7
7
  exports.isSchema = isSchema;
8
8
  var _lodash = require("lodash");
9
9
  var _errors = require("./errors");
10
+ var _formats = require("./formats");
10
11
  var _utils = require("./utils");
11
12
  const INITIAL_TYPES = ['default', 'required', 'type', 'transform', 'empty'];
12
13
  const REQUIRED_TYPES = ['default', 'required', 'missing'];
@@ -22,13 +23,14 @@ class Schema {
22
23
  // Public
23
24
 
24
25
  /**
26
+ * @param {boolean} [allow=true]
25
27
  * @returns {this}
26
28
  */
27
- required() {
29
+ required(allow = true) {
28
30
  return this.clone({
29
- required: true
30
- }).assert('required', val => {
31
- if (val === undefined) {
31
+ required: allow
32
+ }).assert('required', (val, options) => {
33
+ if (val === undefined && options.required) {
32
34
  throw new _errors.LocalizedError('Value is required.');
33
35
  }
34
36
  });
@@ -106,11 +108,12 @@ class Schema {
106
108
 
107
109
  /**
108
110
  * Allow null. [Link](https://github.com/bedrockio/yada#nullable)
111
+ * @param {boolean} [allow=true]
109
112
  * @returns {this}
110
113
  */
111
- nullable() {
114
+ nullable(allow = true) {
112
115
  return this.clone({
113
- nullable: true
116
+ nullable: allow
114
117
  });
115
118
  }
116
119
 
@@ -217,22 +220,22 @@ class Schema {
217
220
 
218
221
  /**
219
222
  * Exports the schema in [JSON Schema](https://json-schema.org/) format.
220
- * Note that this may not represent the schema in its enitrety. Specifically,
223
+ * Note that this may not represent the schema in its entirety. Specifically,
221
224
  * custom (code-based) assertions will not be output.
222
- * @param {Object} [extra]
225
+ * @param {Object} [options]
226
+ * @param {Function} [options.tag] - Allows adding additional custom tags.
227
+ * @param {'openai'} [options.style] - Constrains schema output. Currently only
228
+ * supports OpenAI which will ensure the resulting schema works with the Structured
229
+ * Outputs API.
230
+ * @param {boolean} [options.stripExtensions] - Strips out JSON schema extensions.
223
231
  */
224
- toJsonSchema(extra) {
225
- const {
226
- tags
227
- } = this.meta;
232
+ toJsonSchema(options) {
228
233
  return {
229
- ...tags,
230
- ...this.getAnyType(),
234
+ ...this.getType(),
231
235
  ...this.getDefault(),
232
- ...this.getNullable(),
233
- ...this.getFormat(),
234
- ...this.getEnum(),
235
- ...this.expandExtra(extra)
236
+ ...this.getEnum(options),
237
+ ...this.getTags(options),
238
+ ...this.getFormat(options)
236
239
  };
237
240
  }
238
241
 
@@ -240,8 +243,8 @@ class Schema {
240
243
  * Exports the schema in [JSON Schema](https://json-schema.org/) format.
241
244
  * @alias toJsonSchema.
242
245
  */
243
- toOpenApi(...extra) {
244
- return this.toJsonSchema(...extra);
246
+ toOpenApi(options) {
247
+ return this.toJsonSchema(options);
245
248
  }
246
249
 
247
250
  /**
@@ -250,22 +253,26 @@ class Schema {
250
253
  toJSON() {
251
254
  return this.toJsonSchema();
252
255
  }
253
- getAnyType() {
256
+ getType() {
254
257
  const {
255
258
  type,
256
- enum: set
259
+ nullable
257
260
  } = this.meta;
258
- if (!type && !set) {
261
+ if (type && nullable) {
262
+ return {
263
+ type: [type, 'null']
264
+ };
265
+ } else if (type) {
259
266
  return {
260
- type: ['object', 'array', 'string', 'number', 'boolean', 'null']
267
+ type
261
268
  };
262
269
  }
263
270
  }
264
- getFormat() {
271
+ getFormat(options) {
265
272
  const {
266
273
  format
267
274
  } = this.meta;
268
- if (format) {
275
+ if ((0, _formats.isAllowedFormat)(format, options)) {
269
276
  return {
270
277
  format
271
278
  };
@@ -283,17 +290,7 @@ class Schema {
283
290
  };
284
291
  }
285
292
  }
286
- getNullable() {
287
- const {
288
- nullable
289
- } = this.meta;
290
- if (nullable) {
291
- return {
292
- nullable: true
293
- };
294
- }
295
- }
296
- getEnum() {
293
+ getEnum(options) {
297
294
  const {
298
295
  enum: allowed
299
296
  } = this.meta;
@@ -318,7 +315,7 @@ class Schema {
318
315
  const anyOf = [];
319
316
  for (let entry of allowed) {
320
317
  if (isSchema(entry)) {
321
- anyOf.push(entry.toJsonSchema());
318
+ anyOf.push(entry.toJsonSchema(options));
322
319
  } else if (entry === null) {
323
320
  anyOf.push({
324
321
  type: 'null'
@@ -346,40 +343,55 @@ class Schema {
346
343
  }
347
344
  }
348
345
  }
346
+ getTags(options = {}) {
347
+ const {
348
+ tag,
349
+ stripExtensions
350
+ } = options;
351
+ const tags = {
352
+ ...this.meta.tags
353
+ };
354
+
355
+ // Allows custom tags by options.
356
+ if (typeof tag === 'function') {
357
+ Object.assign(tags, tag(this.meta));
358
+ }
359
+
360
+ // Strip custom extensions by option.
361
+ if (stripExtensions) {
362
+ for (let key of Object.keys(tags)) {
363
+ if (key.startsWith('x-')) {
364
+ delete tags[key];
365
+ }
366
+ }
367
+ }
368
+ return tags;
369
+ }
349
370
 
350
371
  /**
351
- * Augments the schema to make all fields required
352
- * including fields in all nested schemas.
372
+ * Transforms any nested schemas.
373
+ * @param {Function} fn - Transform function that accepts an instance
374
+ * of the schema.
353
375
  * @returns {this}
354
376
  */
355
- requireAllWithin() {
377
+ transform(fn, root) {
356
378
  let {
357
379
  enum: allowed
358
380
  } = this.meta;
359
381
  if (allowed) {
360
382
  allowed = allowed.map(el => {
361
- if (el?.requireAllWithin) {
362
- return el.requireAllWithin();
383
+ if (el?.transform) {
384
+ return el.transform(fn, root);
363
385
  } else {
364
386
  return el;
365
387
  }
366
388
  });
367
- return this.clone({
389
+ return fn(this.clone({
368
390
  enum: allowed
369
- }).required();
391
+ }));
370
392
  } else {
371
- return this.required();
372
- }
373
- }
374
- expandExtra(extra = {}) {
375
- const {
376
- tag,
377
- ...rest
378
- } = extra;
379
- if (typeof extra?.tag === 'function') {
380
- Object.assign(rest, extra.tag(this.meta));
393
+ return fn(this) || this;
381
394
  }
382
- return rest;
383
395
  }
384
396
  inspect() {
385
397
  return JSON.stringify(this, null, 2);
@@ -474,7 +486,7 @@ class Schema {
474
486
  /**
475
487
  * @returns {this}
476
488
  */
477
- transform(fn) {
489
+ transformValue(fn) {
478
490
  this.assert('transform', (val, options) => {
479
491
  if (val !== undefined) {
480
492
  return fn(val, options);
@@ -22,11 +22,5 @@ class TypeSchema extends _Schema.default {
22
22
  toString() {
23
23
  return this.meta.type;
24
24
  }
25
- toJsonSchema(extra) {
26
- return {
27
- ...super.toJsonSchema(extra),
28
- type: this.meta.type
29
- };
30
- }
31
25
  }
32
26
  exports.default = TypeSchema;
package/dist/cjs/array.js CHANGED
@@ -108,20 +108,26 @@ class ArraySchema extends _TypeSchema.default {
108
108
  }
109
109
 
110
110
  /**
111
- * Augments the array schema to make all nested fields required.
111
+ * Recursively transforms all fields in the array schema.
112
+ * @param {Function} fn - Transform function that accepts an instance
113
+ * of the schema.
112
114
  * @returns {this}
113
115
  */
114
- requireAllWithin() {
116
+ transform(fn, root = true) {
115
117
  const {
116
118
  schemas,
117
119
  ...rest
118
120
  } = this.meta;
119
121
  const newSchemas = schemas.map(schema => {
120
- return schema.requireAllWithin();
122
+ return schema.transform(fn, false);
121
123
  });
122
-
123
- // @ts-ignore
124
- return new ArraySchema(newSchemas, rest).required();
124
+ const transformed = new ArraySchema(newSchemas, rest);
125
+ if (root) {
126
+ // @ts-ignore
127
+ return transformed;
128
+ } else {
129
+ return super.transform.call(transformed, fn);
130
+ }
125
131
  }
126
132
 
127
133
  // Private
@@ -129,7 +135,7 @@ class ArraySchema extends _TypeSchema.default {
129
135
  toString() {
130
136
  return 'array';
131
137
  }
132
- toJsonSchema(extra) {
138
+ toJsonSchema(options) {
133
139
  let other;
134
140
  const {
135
141
  schemas
@@ -137,18 +143,17 @@ class ArraySchema extends _TypeSchema.default {
137
143
  if (schemas.length > 1) {
138
144
  other = {
139
145
  anyOf: schemas.map(schema => {
140
- return schema.toJsonSchema();
146
+ return schema.toJsonSchema(options);
141
147
  })
142
148
  };
143
149
  } else if (schemas.length === 1) {
144
150
  other = {
145
- items: schemas[0].toJsonSchema()
151
+ items: schemas[0].toJsonSchema(options)
146
152
  };
147
153
  }
148
154
  return {
149
- ...super.toJsonSchema(extra),
150
- ...other,
151
- type: 'array'
155
+ ...super.toJsonSchema(options),
156
+ ...other
152
157
  };
153
158
  }
154
159
  }
package/dist/cjs/date.js CHANGED
@@ -113,8 +113,9 @@ class DateSchema extends _Schema.default {
113
113
  }
114
114
  timestamp() {
115
115
  return this.clone({
116
- format: 'timestamp'
117
- }).assert('format', (date, options) => {
116
+ type: 'integer',
117
+ format: null
118
+ }).custom((arg, options) => {
118
119
  const {
119
120
  original
120
121
  } = options;
@@ -125,8 +126,9 @@ class DateSchema extends _Schema.default {
125
126
  }
126
127
  unix() {
127
128
  return this.clone({
128
- format: 'unix timestamp'
129
- }).assert('format', (date, options) => {
129
+ type: 'integer',
130
+ format: null
131
+ }).custom((arg, options) => {
130
132
  const {
131
133
  original
132
134
  } = options;
@@ -142,15 +144,6 @@ class DateSchema extends _Schema.default {
142
144
  toString() {
143
145
  return 'date';
144
146
  }
145
- toJsonSchema(extra) {
146
- const {
147
- format
148
- } = this.meta;
149
- return {
150
- ...super.toJsonSchema(extra),
151
- type: format.includes('timestamp') ? 'number' : 'string'
152
- };
153
- }
154
147
  getDefault() {
155
148
  const {
156
149
  default: defaultValue
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isAllowedFormat = isAllowedFormat;
7
+ exports.isOpenAiAllowedFormat = isOpenAiAllowedFormat;
8
+ // Confirmed working as of: 2025-11-11
9
+ // Tested with gpt-5-nano-2025-08-07
10
+ const OPENAI_ALLOWED_FORMATS = ['date-time', 'date', 'time', 'email', 'hostname', 'ipv4', 'ipv6', 'uuid'];
11
+ function isAllowedFormat(format, options = {}) {
12
+ const {
13
+ style
14
+ } = options;
15
+ if (style === 'openai') {
16
+ // OpenAI schema inputs will fail on unknown formats.
17
+ // However these formats also offer useful guides for
18
+ // the LLM so we want to keep them as much as possible.
19
+ return isOpenAiAllowedFormat(format);
20
+ } else if (format) {
21
+ // JSON Schema has pre-defined types but also allows
22
+ // custom formats so by default allow any format.
23
+ return true;
24
+ }
25
+ }
26
+ function isOpenAiAllowedFormat(format) {
27
+ return OPENAI_ALLOWED_FORMATS.includes(format);
28
+ }
@@ -59,7 +59,9 @@ class NumberSchema extends _TypeSchema.default {
59
59
  return this.min(0, 'Must be positive.');
60
60
  }
61
61
  integer() {
62
- return this.clone().assert('integer', num => {
62
+ return this.clone({
63
+ type: 'integer'
64
+ }).assert('integer', num => {
63
65
  if (!Number.isInteger(num)) {
64
66
  throw new _errors.LocalizedError('Must be an integer.');
65
67
  }
@@ -79,14 +81,14 @@ class NumberSchema extends _TypeSchema.default {
79
81
 
80
82
  // Private
81
83
 
82
- toJsonSchema(extra) {
84
+ toJsonSchema(options) {
83
85
  const {
84
86
  min,
85
87
  max,
86
88
  multiple
87
89
  } = this.meta;
88
90
  return {
89
- ...super.toJsonSchema(extra),
91
+ ...super.toJsonSchema(options),
90
92
  ...(min != null && {
91
93
  minimum: min
92
94
  }),
@@ -29,7 +29,7 @@ class ObjectSchema extends _TypeSchema.default {
29
29
  throw new _errors.LocalizedError('Must be an object.');
30
30
  }
31
31
  });
32
- this.transform((obj, options) => {
32
+ this.transformValue((obj, options) => {
33
33
  const {
34
34
  stripUnknown,
35
35
  stripEmpty
@@ -219,18 +219,24 @@ class ObjectSchema extends _TypeSchema.default {
219
219
  }
220
220
 
221
221
  /**
222
- * Augments the object schema to make all fields required
223
- * including fields in all nested schemas.
222
+ * Recursively transforms all fields including those in
223
+ * nested schemas.
224
+ * @param {Function} fn - Transform function that accepts an instance
225
+ * of the schema.
224
226
  * @returns {this}
225
227
  */
226
- requireAllWithin() {
228
+ transform(fn, root = true) {
227
229
  const update = {};
228
- for (let field of Object.keys(this.meta.fields)) {
229
- (0, _lodash.set)(update, field, this.get(field).requireAllWithin());
230
+ for (let field of Object.keys(this.meta.fields || {})) {
231
+ (0, _lodash.set)(update, field, this.get(field).transform(fn, false));
232
+ }
233
+ const transformed = this.append(update);
234
+ if (root) {
235
+ // @ts-ignore
236
+ return transformed;
237
+ } else {
238
+ return super.transform.call(transformed, fn);
230
239
  }
231
-
232
- // @ts-ignore
233
- return this.append(update).required();
234
240
  }
235
241
 
236
242
  /**
@@ -300,20 +306,20 @@ class ObjectSchema extends _TypeSchema.default {
300
306
 
301
307
  // Private
302
308
 
303
- toJsonSchema(extra) {
309
+ toJsonSchema(options) {
304
310
  const {
305
311
  stripUnknown = false
306
312
  } = this.meta;
307
313
  const required = [];
308
314
  const properties = {};
309
315
  for (let [key, schema] of Object.entries(this.export())) {
310
- properties[key] = schema.toJsonSchema(extra);
316
+ properties[key] = schema.toJsonSchema(options);
311
317
  if (schema.meta.required) {
312
318
  required.push(key);
313
319
  }
314
320
  }
315
321
  return {
316
- ...super.toJsonSchema(extra),
322
+ ...super.toJsonSchema(options),
317
323
  ...(Object.keys(properties).length > 0 && {
318
324
  properties,
319
325
  required,
@@ -41,19 +41,6 @@ class StringSchema extends _TypeSchema.default {
41
41
  });
42
42
  }
43
43
 
44
- /**
45
- * @returns {this}
46
- */
47
- required() {
48
- return this.clone({
49
- required: true
50
- }).assert('required', val => {
51
- if (val == null) {
52
- throw new _errors.LocalizedError('Value is required.');
53
- }
54
- });
55
- }
56
-
57
44
  /**
58
45
  * @param {number} length
59
46
  */
@@ -99,7 +86,7 @@ class StringSchema extends _TypeSchema.default {
99
86
  });
100
87
  }
101
88
  trim() {
102
- return this.clone().transform(str => {
89
+ return this.clone().transformValue(str => {
103
90
  return str.trim();
104
91
  });
105
92
  }
@@ -108,7 +95,7 @@ class StringSchema extends _TypeSchema.default {
108
95
  * @param {boolean} [assert] Throws an error if not lowercase. Default: `false`.
109
96
  */
110
97
  lowercase(assert = false) {
111
- return this.clone().transform(str => {
98
+ return this.clone().transformValue(str => {
112
99
  const lower = str.toLowerCase();
113
100
  if (lower !== str) {
114
101
  if (assert) {
@@ -123,7 +110,7 @@ class StringSchema extends _TypeSchema.default {
123
110
  * @param {boolean} [assert] Throws an error if not uppercase. Default: `false`.
124
111
  */
125
112
  uppercase(assert = false) {
126
- return this.clone().transform(str => {
113
+ return this.clone().transformValue(str => {
127
114
  const upper = str.toUpperCase();
128
115
  if (upper !== str) {
129
116
  if (assert) {
@@ -432,13 +419,13 @@ class StringSchema extends _TypeSchema.default {
432
419
 
433
420
  // Private
434
421
 
435
- toJsonSchema(extra) {
422
+ toJsonSchema(options) {
436
423
  const {
437
424
  min,
438
425
  max
439
426
  } = this.meta;
440
427
  return {
441
- ...super.toJsonSchema(extra),
428
+ ...super.toJsonSchema(options),
442
429
  ...(min && {
443
430
  minLength: min
444
431
  }),
package/dist/cjs/tuple.js CHANGED
@@ -10,7 +10,8 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
10
10
  class TupleSchema extends _Schema.default {
11
11
  constructor(schemas) {
12
12
  super({
13
- schemas
13
+ schemas,
14
+ type: 'array'
14
15
  });
15
16
  this.setup();
16
17
  }
@@ -79,15 +80,14 @@ class TupleSchema extends _Schema.default {
79
80
  toString() {
80
81
  return 'tuple';
81
82
  }
82
- toJsonSchema(extra) {
83
+ toJsonSchema(options) {
83
84
  const {
84
85
  schemas
85
86
  } = this.meta;
86
87
  return {
87
- ...super.toJsonSchema(extra),
88
- type: 'array',
88
+ ...super.toJsonSchema(options),
89
89
  prefixItems: schemas.map(schema => {
90
- return schema.toJsonSchema();
90
+ return schema.toJsonSchema(options);
91
91
  })
92
92
  };
93
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrockio/yada",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Validation library inspired by Joi.",
5
5
  "scripts": {
6
6
  "test": "jest",