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 +81 -4
- package/cjs/index.d.ts +11 -6
- package/cjs/index.js +149 -18
- package/dist/json-validation.js +121 -18
- package/dist/types/format-validators.d.ts +1 -1
- package/dist/types/z-schema.d.ts +10 -5
- package/dist/z-schema.js +28 -0
- package/package.json +5 -6
- package/src/format-validators.ts +1 -1
- package/src/json-validation.ts +140 -20
- package/src/z-schema.ts +38 -4
- package/umd/ZSchema.js +149 -18
- package/umd/ZSchema.min.js +1 -1
package/README.md
CHANGED
|
@@ -67,12 +67,89 @@ var errors = validator.getLastErrors();
|
|
|
67
67
|
...
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
### Async
|
|
70
|
+
### Async validation:
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
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 = (
|
|
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--
|
|
1881
|
+
while (idx--) {
|
|
1883
1882
|
const subReport = new Report(report);
|
|
1884
1883
|
subReports.push(subReport);
|
|
1885
|
-
|
|
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
|
-
|
|
1888
|
-
|
|
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
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
report.
|
|
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
|
|
1907
|
-
|
|
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
|
-
|
|
1947
|
-
if (
|
|
1948
|
-
|
|
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
|
*/
|
package/dist/json-validation.js
CHANGED
|
@@ -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--
|
|
375
|
+
while (idx--) {
|
|
377
376
|
const subReport = new Report(report);
|
|
378
377
|
subReports.push(subReport);
|
|
379
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
report.
|
|
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
|
|
401
|
-
|
|
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
|
-
|
|
441
|
-
if (
|
|
442
|
-
|
|
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
|
}
|
package/dist/types/z-schema.d.ts
CHANGED
|
@@ -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 = (
|
|
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.
|
|
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:
|
|
71
|
-
"
|
|
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",
|
package/src/format-validators.ts
CHANGED
|
@@ -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') {
|