z-schema 8.4.0 → 8.5.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/README.md CHANGED
@@ -67,12 +67,89 @@ var errors = validator.getLastErrors();
67
67
  ...
68
68
  ```
69
69
 
70
- ### Async mode:
70
+ ### Async validation:
71
71
 
72
- ```javascript
73
- validator.validate(json, schema, function (err, valid) {
74
- ...
72
+ ZSchema supports custom format validators that can perform both synchronous and asynchronous validation. This example shows how to validate a person payload with:
73
+
74
+ - **Async validation**: User ID against a database
75
+ - **Async validation**: Postcode against an external service
76
+ - **Sync validation**: Phone number format
77
+
78
+ ```typescript
79
+ import ZSchema from 'z-schema';
80
+ import db from './db';
81
+
82
+ // Initialize ZSchema
83
+ const validator = new ZSchema();
84
+
85
+ // Register async and sync format validators
86
+ validator.registerFormat('user-exists', async (input: unknown): Promise<boolean> => {
87
+ if (typeof input !== 'number') return false;
88
+ const user = await db.getUserById(input);
89
+ return user != null;
75
90
  });
91
+ validator.registerFormat('valid-postcode', async (input: unknown): Promise<boolean> => {
92
+ if (typeof input !== 'string') return false;
93
+ const postcode = await db.getPostcode(input);
94
+ return postcode != null;
95
+ });
96
+ validator.registerFormat('phone-number', (input: unknown): boolean => {
97
+ if (typeof input !== 'string') return false;
98
+ const phoneRegex = /^\+?[1-9]\d{1,14}$/;
99
+ return phoneRegex.test(input);
100
+ });
101
+
102
+ // Define the JSON Schema
103
+ const personSchema = {
104
+ $schema: 'http://json-schema.org/draft-04/schema#',
105
+ type: 'object',
106
+ required: ['personId', 'address'],
107
+ properties: {
108
+ personId: {
109
+ type: 'number',
110
+ format: 'user-exists',
111
+ },
112
+ address: {
113
+ type: 'object',
114
+ required: ['postcode', 'phone'],
115
+ properties: {
116
+ postcode: {
117
+ type: 'string',
118
+ format: 'valid-postcode',
119
+ },
120
+ phone: {
121
+ type: 'string',
122
+ format: 'phone-number',
123
+ },
124
+ },
125
+ },
126
+ },
127
+ };
128
+
129
+ // Example payload
130
+ const payload = {
131
+ personId: 'user123',
132
+ address: {
133
+ postcode: 'SW1A 1AA',
134
+ phone: '+441234567890',
135
+ },
136
+ };
137
+
138
+ // Validate asynchronously
139
+ try {
140
+ await validator.validateAsync(payload, personSchema);
141
+ console.log('✅ Validation successful!');
142
+ } catch (err) {
143
+ console.log('❌ Validation failed:', err);
144
+ }
145
+
146
+ // or validate without try-catch
147
+ const res = await validator.validateAsyncSafe(payload, personSchema);
148
+ if (res.valid) {
149
+ console.log('✅ Validation successful!');
150
+ } else {
151
+ console.log('❌ Validation failed:', res.errs);
152
+ }
76
153
  ```
77
154
 
78
155
  ### Browser:
package/cjs/index.d.ts CHANGED
@@ -200,7 +200,7 @@ declare class Report {
200
200
  addCustomError(errorCode: ErrorCode, errorMessage: string, params?: ErrorParam[], subReports?: Report | Report[], schema?: JsonSchema, keyword?: keyof JsonSchema): void;
201
201
  }
202
202
 
203
- type FormatValidatorFn = (input: unknown) => boolean;
203
+ type FormatValidatorFn = (input: unknown) => boolean | Promise<boolean>;
204
204
  interface FormatValidatorsOptions {
205
205
  strictUris?: boolean;
206
206
  customFormats?: Record<string, FormatValidatorFn | null>;
@@ -274,7 +274,7 @@ interface ValidateOptions {
274
274
  includeErrors?: Array<keyof typeof Errors>;
275
275
  excludeErrors?: Array<keyof typeof Errors>;
276
276
  }
277
- type ValidateCallback = (e: Error | SchemaErrorDetail[] | null, valid: boolean) => void;
277
+ type ValidateCallback = (err: Error | SchemaErrorDetail[] | null, valid: boolean) => void;
278
278
  type SchemaReader = (uri: string) => JsonSchema;
279
279
  declare class ZSchema {
280
280
  static registerFormat(name: string, validatorFunction: FormatValidatorFn): void;
@@ -297,6 +297,11 @@ declare class ZSchema {
297
297
  validate(json: unknown, schema: JsonSchema | string, callback: ValidateCallback): void;
298
298
  validate(json: unknown, schema: JsonSchema | string, options: ValidateOptions): boolean;
299
299
  validate(json: unknown, schema: JsonSchema | string): boolean;
300
+ validateAsync(json: unknown, schema: JsonSchema | string, options?: ValidateOptions): Promise<true>;
301
+ validateAsyncSafe(json: unknown, schema: JsonSchema | string, options?: ValidateOptions): Promise<{
302
+ valid: boolean;
303
+ errs?: any[];
304
+ }>;
300
305
  /**
301
306
  * Returns an Error object for the most recent failed validation, or null if the validation was successful.
302
307
  */
@@ -312,10 +317,10 @@ declare class ZSchema {
312
317
  getMissingReferences(arr?: SchemaErrorDetail[]): string[];
313
318
  getMissingRemoteReferences(): string[];
314
319
  getResolvedSchema(schema: JsonSchema): JsonSchema;
315
- static schemaReader: SchemaReader;
316
- setSchemaReader(schemaReader: SchemaReader): void;
317
- getSchemaReader(): SchemaReader;
318
- static setSchemaReader(schemaReader: SchemaReader): void;
320
+ static schemaReader: SchemaReader | undefined;
321
+ setSchemaReader(schemaReader: SchemaReader | undefined): void;
322
+ getSchemaReader(): SchemaReader | undefined;
323
+ static setSchemaReader(schemaReader: SchemaReader | undefined): void;
319
324
  static schemaSymbol: symbol;
320
325
  static jsonSymbol: symbol;
321
326
  }
package/cjs/index.js CHANGED
@@ -1877,34 +1877,106 @@ const JsonValidators = {
1877
1877
  anyOf: function (report, schema, json) {
1878
1878
  // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.4.2
1879
1879
  const subReports = [];
1880
- let passed = false;
1881
1880
  let idx = schema.anyOf.length;
1882
- while (idx-- && passed === false) {
1881
+ while (idx--) {
1883
1882
  const subReport = new Report(report);
1884
1883
  subReports.push(subReport);
1885
- passed = validate.call(this, subReport, schema.anyOf[idx], json);
1884
+ validate.call(this, subReport, schema.anyOf[idx], json);
1885
+ }
1886
+ // Aggregate async tasks from sub-reports to the main report
1887
+ const asyncTasksBefore = report.asyncTasks.length;
1888
+ for (const subReport of subReports) {
1889
+ report.asyncTasks.push(...subReport.asyncTasks);
1890
+ }
1891
+ const hasAsyncTasks = report.asyncTasks.length > asyncTasksBefore;
1892
+ if (hasAsyncTasks) {
1893
+ // Defer the decision until async tasks complete
1894
+ const pathBeforeAsync = shallowClone(report.path);
1895
+ report.addAsyncTask((callback) => {
1896
+ setTimeout(() => callback(null), 0);
1897
+ }, [], () => {
1898
+ const backup = report.path;
1899
+ report.path = pathBeforeAsync;
1900
+ let passed = false;
1901
+ for (const subReport of subReports) {
1902
+ if (subReport.errors.length === 0) {
1903
+ passed = true;
1904
+ break;
1905
+ }
1906
+ }
1907
+ if (passed === false) {
1908
+ report.addError('ANY_OF_MISSING', undefined, subReports, schema, 'anyOf');
1909
+ }
1910
+ report.path = backup;
1911
+ });
1886
1912
  }
1887
- if (passed === false) {
1888
- report.addError('ANY_OF_MISSING', undefined, subReports, schema, 'anyOf');
1913
+ else {
1914
+ // No async tasks, decide immediately
1915
+ let passed = false;
1916
+ for (const subReport of subReports) {
1917
+ if (subReport.errors.length === 0) {
1918
+ passed = true;
1919
+ break;
1920
+ }
1921
+ }
1922
+ if (passed === false) {
1923
+ report.addError('ANY_OF_MISSING', undefined, subReports, schema, 'anyOf');
1924
+ }
1889
1925
  }
1890
1926
  },
1891
1927
  oneOf: function (report, schema, json) {
1892
1928
  // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.5.2
1893
- let passes = 0;
1894
1929
  const subReports = [];
1895
1930
  let idx = schema.oneOf.length;
1896
1931
  while (idx--) {
1897
1932
  const subReport = new Report(report);
1898
1933
  subReports.push(subReport);
1899
- if (validate.call(this, subReport, schema.oneOf[idx], json) === true) {
1900
- passes++;
1901
- }
1902
- }
1903
- if (passes === 0) {
1904
- report.addError('ONE_OF_MISSING', undefined, subReports, schema, 'oneOf');
1934
+ validate.call(this, subReport, schema.oneOf[idx], json);
1935
+ }
1936
+ // Aggregate async tasks from sub-reports to the main report
1937
+ const asyncTasksBefore = report.asyncTasks.length;
1938
+ for (const subReport of subReports) {
1939
+ report.asyncTasks.push(...subReport.asyncTasks);
1940
+ }
1941
+ const hasAsyncTasks = report.asyncTasks.length > asyncTasksBefore;
1942
+ if (hasAsyncTasks) {
1943
+ // Defer the decision until async tasks complete
1944
+ const pathBeforeAsync = shallowClone(report.path);
1945
+ report.addAsyncTask((callback) => {
1946
+ // This task runs after all async tasks, so we can check final state
1947
+ setTimeout(() => callback(null), 0);
1948
+ }, [], () => {
1949
+ const backup = report.path;
1950
+ report.path = pathBeforeAsync;
1951
+ let passes = 0;
1952
+ for (const subReport of subReports) {
1953
+ if (subReport.errors.length === 0) {
1954
+ passes++;
1955
+ }
1956
+ }
1957
+ if (passes === 0) {
1958
+ report.addError('ONE_OF_MISSING', undefined, subReports, schema, 'oneOf');
1959
+ }
1960
+ else if (passes > 1) {
1961
+ report.addError('ONE_OF_MULTIPLE', undefined, undefined, schema, 'oneOf');
1962
+ }
1963
+ report.path = backup;
1964
+ });
1905
1965
  }
1906
- else if (passes > 1) {
1907
- report.addError('ONE_OF_MULTIPLE', undefined, undefined, schema, 'oneOf');
1966
+ else {
1967
+ // No async tasks, decide immediately
1968
+ let passes = 0;
1969
+ for (const subReport of subReports) {
1970
+ if (subReport.errors.length === 0) {
1971
+ passes++;
1972
+ }
1973
+ }
1974
+ if (passes === 0) {
1975
+ report.addError('ONE_OF_MISSING', undefined, subReports, schema, 'oneOf');
1976
+ }
1977
+ else if (passes > 1) {
1978
+ report.addError('ONE_OF_MULTIPLE', undefined, undefined, schema, 'oneOf');
1979
+ }
1908
1980
  }
1909
1981
  },
1910
1982
  not: function (report, schema, json) {
@@ -1931,7 +2003,7 @@ const JsonValidators = {
1931
2003
  return;
1932
2004
  }
1933
2005
  if (formatValidatorFn.length === 2) {
1934
- // async - need to clone the path here, because it will change by the time async function reports back
2006
+ // callback-based async - need to clone the path here, because it will change by the time async function reports back
1935
2007
  const pathBeforeAsync = shallowClone(report.path);
1936
2008
  report.addAsyncTask(formatValidatorFn, [json], function (result) {
1937
2009
  if (result !== true) {
@@ -1943,9 +2015,40 @@ const JsonValidators = {
1943
2015
  });
1944
2016
  }
1945
2017
  else {
1946
- // sync
1947
- if (formatValidatorFn.call(this, json) !== true) {
1948
- report.addError('INVALID_FORMAT', [schema.format, JSON.stringify(json)], undefined, schema, 'format');
2018
+ const result = formatValidatorFn.call(this, json);
2019
+ if (result instanceof Promise) {
2020
+ // Promise-based async
2021
+ const pathBeforeAsync = shallowClone(report.path);
2022
+ const timeoutMs = this.options.asyncTimeout || 2000;
2023
+ report.addAsyncTask(async (callback) => {
2024
+ try {
2025
+ const timeoutPromise = new Promise((_, reject) => {
2026
+ setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
2027
+ });
2028
+ const resolved = await Promise.race([result, timeoutPromise]);
2029
+ callback(resolved);
2030
+ }
2031
+ catch (error) {
2032
+ if (error.message === 'Async timeout') {
2033
+ // Don't call callback, let global timeout handle it
2034
+ return;
2035
+ }
2036
+ callback(false);
2037
+ }
2038
+ }, [], function (resolvedResult) {
2039
+ if (resolvedResult !== true) {
2040
+ const backup = report.path;
2041
+ report.path = pathBeforeAsync;
2042
+ report.addError('INVALID_FORMAT', [schema.format, JSON.stringify(json)], undefined, schema, 'format');
2043
+ report.path = backup;
2044
+ }
2045
+ });
2046
+ }
2047
+ else {
2048
+ // sync
2049
+ if (result !== true) {
2050
+ report.addError('INVALID_FORMAT', [schema.format, JSON.stringify(json)], undefined, schema, 'format');
2051
+ }
1949
2052
  }
1950
2053
  }
1951
2054
  }
@@ -5767,6 +5870,34 @@ class ZSchema {
5767
5870
  this.lastReport = report;
5768
5871
  return report.isValid();
5769
5872
  }
5873
+ // validateAsync always returns true, implement using try-catch
5874
+ validateAsync(json, schema, options) {
5875
+ return new Promise((resolve, reject) => {
5876
+ try {
5877
+ this.validate(json, schema, options || {}, (err, valid) => err || valid !== true ? reject(err) : resolve(valid));
5878
+ }
5879
+ catch (err) {
5880
+ reject(err);
5881
+ }
5882
+ });
5883
+ }
5884
+ // validateAsyncSafe never throws, but returns complex object
5885
+ validateAsyncSafe(json, schema, options) {
5886
+ return new Promise((resolve) => {
5887
+ try {
5888
+ this.validate(json, schema, options || {}, (err, valid) => {
5889
+ let errs;
5890
+ if (err != null) {
5891
+ errs = Array.isArray(err) ? err : [err];
5892
+ }
5893
+ resolve({ valid, errs });
5894
+ });
5895
+ }
5896
+ catch (err) {
5897
+ resolve({ valid: false, errs: Array.isArray(err) ? err : [err] });
5898
+ }
5899
+ });
5900
+ }
5770
5901
  /**
5771
5902
  * Returns an Error object for the most recent failed validation, or null if the validation was successful.
5772
5903
  */
@@ -371,34 +371,106 @@ export const JsonValidators = {
371
371
  anyOf: function (report, schema, json) {
372
372
  // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.4.2
373
373
  const subReports = [];
374
- let passed = false;
375
374
  let idx = schema.anyOf.length;
376
- while (idx-- && passed === false) {
375
+ while (idx--) {
377
376
  const subReport = new Report(report);
378
377
  subReports.push(subReport);
379
- passed = validate.call(this, subReport, schema.anyOf[idx], json);
378
+ validate.call(this, subReport, schema.anyOf[idx], json);
379
+ }
380
+ // Aggregate async tasks from sub-reports to the main report
381
+ const asyncTasksBefore = report.asyncTasks.length;
382
+ for (const subReport of subReports) {
383
+ report.asyncTasks.push(...subReport.asyncTasks);
384
+ }
385
+ const hasAsyncTasks = report.asyncTasks.length > asyncTasksBefore;
386
+ if (hasAsyncTasks) {
387
+ // Defer the decision until async tasks complete
388
+ const pathBeforeAsync = shallowClone(report.path);
389
+ report.addAsyncTask((callback) => {
390
+ setTimeout(() => callback(null), 0);
391
+ }, [], () => {
392
+ const backup = report.path;
393
+ report.path = pathBeforeAsync;
394
+ let passed = false;
395
+ for (const subReport of subReports) {
396
+ if (subReport.errors.length === 0) {
397
+ passed = true;
398
+ break;
399
+ }
400
+ }
401
+ if (passed === false) {
402
+ report.addError('ANY_OF_MISSING', undefined, subReports, schema, 'anyOf');
403
+ }
404
+ report.path = backup;
405
+ });
380
406
  }
381
- if (passed === false) {
382
- report.addError('ANY_OF_MISSING', undefined, subReports, schema, 'anyOf');
407
+ else {
408
+ // No async tasks, decide immediately
409
+ let passed = false;
410
+ for (const subReport of subReports) {
411
+ if (subReport.errors.length === 0) {
412
+ passed = true;
413
+ break;
414
+ }
415
+ }
416
+ if (passed === false) {
417
+ report.addError('ANY_OF_MISSING', undefined, subReports, schema, 'anyOf');
418
+ }
383
419
  }
384
420
  },
385
421
  oneOf: function (report, schema, json) {
386
422
  // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.5.2
387
- let passes = 0;
388
423
  const subReports = [];
389
424
  let idx = schema.oneOf.length;
390
425
  while (idx--) {
391
426
  const subReport = new Report(report);
392
427
  subReports.push(subReport);
393
- if (validate.call(this, subReport, schema.oneOf[idx], json) === true) {
394
- passes++;
395
- }
396
- }
397
- if (passes === 0) {
398
- report.addError('ONE_OF_MISSING', undefined, subReports, schema, 'oneOf');
428
+ validate.call(this, subReport, schema.oneOf[idx], json);
429
+ }
430
+ // Aggregate async tasks from sub-reports to the main report
431
+ const asyncTasksBefore = report.asyncTasks.length;
432
+ for (const subReport of subReports) {
433
+ report.asyncTasks.push(...subReport.asyncTasks);
434
+ }
435
+ const hasAsyncTasks = report.asyncTasks.length > asyncTasksBefore;
436
+ if (hasAsyncTasks) {
437
+ // Defer the decision until async tasks complete
438
+ const pathBeforeAsync = shallowClone(report.path);
439
+ report.addAsyncTask((callback) => {
440
+ // This task runs after all async tasks, so we can check final state
441
+ setTimeout(() => callback(null), 0);
442
+ }, [], () => {
443
+ const backup = report.path;
444
+ report.path = pathBeforeAsync;
445
+ let passes = 0;
446
+ for (const subReport of subReports) {
447
+ if (subReport.errors.length === 0) {
448
+ passes++;
449
+ }
450
+ }
451
+ if (passes === 0) {
452
+ report.addError('ONE_OF_MISSING', undefined, subReports, schema, 'oneOf');
453
+ }
454
+ else if (passes > 1) {
455
+ report.addError('ONE_OF_MULTIPLE', undefined, undefined, schema, 'oneOf');
456
+ }
457
+ report.path = backup;
458
+ });
399
459
  }
400
- else if (passes > 1) {
401
- report.addError('ONE_OF_MULTIPLE', undefined, undefined, schema, 'oneOf');
460
+ else {
461
+ // No async tasks, decide immediately
462
+ let passes = 0;
463
+ for (const subReport of subReports) {
464
+ if (subReport.errors.length === 0) {
465
+ passes++;
466
+ }
467
+ }
468
+ if (passes === 0) {
469
+ report.addError('ONE_OF_MISSING', undefined, subReports, schema, 'oneOf');
470
+ }
471
+ else if (passes > 1) {
472
+ report.addError('ONE_OF_MULTIPLE', undefined, undefined, schema, 'oneOf');
473
+ }
402
474
  }
403
475
  },
404
476
  not: function (report, schema, json) {
@@ -425,7 +497,7 @@ export const JsonValidators = {
425
497
  return;
426
498
  }
427
499
  if (formatValidatorFn.length === 2) {
428
- // async - need to clone the path here, because it will change by the time async function reports back
500
+ // callback-based async - need to clone the path here, because it will change by the time async function reports back
429
501
  const pathBeforeAsync = shallowClone(report.path);
430
502
  report.addAsyncTask(formatValidatorFn, [json], function (result) {
431
503
  if (result !== true) {
@@ -437,9 +509,40 @@ export const JsonValidators = {
437
509
  });
438
510
  }
439
511
  else {
440
- // sync
441
- if (formatValidatorFn.call(this, json) !== true) {
442
- report.addError('INVALID_FORMAT', [schema.format, JSON.stringify(json)], undefined, schema, 'format');
512
+ const result = formatValidatorFn.call(this, json);
513
+ if (result instanceof Promise) {
514
+ // Promise-based async
515
+ const pathBeforeAsync = shallowClone(report.path);
516
+ const timeoutMs = this.options.asyncTimeout || 2000;
517
+ report.addAsyncTask(async (callback) => {
518
+ try {
519
+ const timeoutPromise = new Promise((_, reject) => {
520
+ setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
521
+ });
522
+ const resolved = await Promise.race([result, timeoutPromise]);
523
+ callback(resolved);
524
+ }
525
+ catch (error) {
526
+ if (error.message === 'Async timeout') {
527
+ // Don't call callback, let global timeout handle it
528
+ return;
529
+ }
530
+ callback(false);
531
+ }
532
+ }, [], function (resolvedResult) {
533
+ if (resolvedResult !== true) {
534
+ const backup = report.path;
535
+ report.path = pathBeforeAsync;
536
+ report.addError('INVALID_FORMAT', [schema.format, JSON.stringify(json)], undefined, schema, 'format');
537
+ report.path = backup;
538
+ }
539
+ });
540
+ }
541
+ else {
542
+ // sync
543
+ if (result !== true) {
544
+ report.addError('INVALID_FORMAT', [schema.format, JSON.stringify(json)], undefined, schema, 'format');
545
+ }
443
546
  }
444
547
  }
445
548
  }
@@ -1,4 +1,4 @@
1
- export type FormatValidatorFn = (input: unknown) => boolean;
1
+ export type FormatValidatorFn = (input: unknown) => boolean | Promise<boolean>;
2
2
  export interface FormatValidatorsOptions {
3
3
  strictUris?: boolean;
4
4
  customFormats?: Record<string, FormatValidatorFn | null>;
@@ -37,7 +37,7 @@ export interface ValidateOptions {
37
37
  includeErrors?: Array<keyof typeof Errors>;
38
38
  excludeErrors?: Array<keyof typeof Errors>;
39
39
  }
40
- export type ValidateCallback = (e: Error | SchemaErrorDetail[] | null, valid: boolean) => void;
40
+ export type ValidateCallback = (err: Error | SchemaErrorDetail[] | null, valid: boolean) => void;
41
41
  export type SchemaReader = (uri: string) => JsonSchema;
42
42
  export declare class ZSchema {
43
43
  static registerFormat(name: string, validatorFunction: FormatValidatorFn): void;
@@ -60,6 +60,11 @@ export declare class ZSchema {
60
60
  validate(json: unknown, schema: JsonSchema | string, callback: ValidateCallback): void;
61
61
  validate(json: unknown, schema: JsonSchema | string, options: ValidateOptions): boolean;
62
62
  validate(json: unknown, schema: JsonSchema | string): boolean;
63
+ validateAsync(json: unknown, schema: JsonSchema | string, options?: ValidateOptions): Promise<true>;
64
+ validateAsyncSafe(json: unknown, schema: JsonSchema | string, options?: ValidateOptions): Promise<{
65
+ valid: boolean;
66
+ errs?: any[];
67
+ }>;
63
68
  /**
64
69
  * Returns an Error object for the most recent failed validation, or null if the validation was successful.
65
70
  */
@@ -75,10 +80,10 @@ export declare class ZSchema {
75
80
  getMissingReferences(arr?: SchemaErrorDetail[]): string[];
76
81
  getMissingRemoteReferences(): string[];
77
82
  getResolvedSchema(schema: JsonSchema): JsonSchema;
78
- static schemaReader: SchemaReader;
79
- setSchemaReader(schemaReader: SchemaReader): void;
80
- getSchemaReader(): SchemaReader;
81
- static setSchemaReader(schemaReader: SchemaReader): void;
83
+ static schemaReader: SchemaReader | undefined;
84
+ setSchemaReader(schemaReader: SchemaReader | undefined): void;
85
+ getSchemaReader(): SchemaReader | undefined;
86
+ static setSchemaReader(schemaReader: SchemaReader | undefined): void;
82
87
  static schemaSymbol: symbol;
83
88
  static jsonSymbol: symbol;
84
89
  }
package/dist/z-schema.js CHANGED
@@ -240,6 +240,34 @@ export class ZSchema {
240
240
  this.lastReport = report;
241
241
  return report.isValid();
242
242
  }
243
+ // validateAsync always returns true, implement using try-catch
244
+ validateAsync(json, schema, options) {
245
+ return new Promise((resolve, reject) => {
246
+ try {
247
+ this.validate(json, schema, options || {}, (err, valid) => err || valid !== true ? reject(err) : resolve(valid));
248
+ }
249
+ catch (err) {
250
+ reject(err);
251
+ }
252
+ });
253
+ }
254
+ // validateAsyncSafe never throws, but returns complex object
255
+ validateAsyncSafe(json, schema, options) {
256
+ return new Promise((resolve) => {
257
+ try {
258
+ this.validate(json, schema, options || {}, (err, valid) => {
259
+ let errs;
260
+ if (err != null) {
261
+ errs = Array.isArray(err) ? err : [err];
262
+ }
263
+ resolve({ valid, errs });
264
+ });
265
+ }
266
+ catch (err) {
267
+ resolve({ valid: false, errs: Array.isArray(err) ? err : [err] });
268
+ }
269
+ });
270
+ }
243
271
  /**
244
272
  * Returns an Error object for the most recent failed validation, or null if the validation was successful.
245
273
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "z-schema",
3
- "version": "8.4.0",
3
+ "version": "8.5.0",
4
4
  "engines": {
5
5
  "node": ">=22.0.0"
6
6
  },
@@ -50,7 +50,7 @@
50
50
  "scripts": {
51
51
  "prepare": "husky",
52
52
  "pre-commit": "npx lint-staged",
53
- "pre-push": "npm run build",
53
+ "pre-push": "npm run build && npm run build:tests",
54
54
  "format": "prettier --write .",
55
55
  "format:check": "prettier --check .",
56
56
  "lint": "eslint --fix",
@@ -59,16 +59,15 @@
59
59
  "build": "npm run copy:schemas && tsc && npm run copy:module-json && rollup -c",
60
60
  "build:browser": "rollup -c --environment BROWSER",
61
61
  "build:watch": "rollup -c -w",
62
+ "build:tests": "tsc --noEmit --project test/tsconfig.json",
62
63
  "copy:module-json": "node -e \"fs.copyFileSync('./src/package.json', './dist/package.json')\"",
63
64
  "copy:schemas": "node ./scripts/copy-schemas.mts",
64
65
  "prepublishOnly": "npm run clean && npm run build && npm test",
65
66
  "test": "vitest run",
66
67
  "test:coverage": "vitest run --coverage",
67
- "test:quick": "npm run build && npm run test:node",
68
- "test:browser": "vitest run --project browser --silent=false",
69
68
  "test:node": "vitest run --project node --silent=false",
70
- "test:sample": "npx vitest run --silent=false --hideSkippedTests --project node -t \"invalid definition\"",
71
- "release:commit": "node -e \"const v=process.argv[1]; if(!v) throw new Error('Usage: npm run release:empty-commit -- <version>'); require('child_process').execSync(`git commit --allow-empty -m \\\"chore: release ${v}\\\" -m \\\"Release-As: ${v}\\\"`, {stdio:'inherit'});\" --"
69
+ "test:browser": "vitest run --project browser --silent=false",
70
+ "test:sample": "npx vitest run --silent=false --hideSkippedTests --project node -t \"invalid definition\""
72
71
  },
73
72
  "dependencies": {
74
73
  "lodash.isequal": "^4.5.0",
@@ -3,7 +3,7 @@ import isIPModule from 'validator/lib/isIP.js';
3
3
  import isURLModule from 'validator/lib/isURL.js';
4
4
  import { sortedKeys } from './utils/json.js';
5
5
 
6
- export type FormatValidatorFn = (input: unknown) => boolean;
6
+ export type FormatValidatorFn = (input: unknown) => boolean | Promise<boolean>;
7
7
 
8
8
  const dateValidator: FormatValidatorFn = (date: unknown) => {
9
9
  if (typeof date !== 'string') {