schema-shield 0.0.2 → 0.0.4
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/LICENSE +201 -0
- package/README.md +761 -6
- package/dist/formats.d.ts +2 -2
- package/dist/formats.d.ts.map +1 -1
- package/dist/index.d.ts +29 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +925 -977
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.mjs +925 -977
- package/dist/keywords/array-keywords.d.ts +2 -2
- package/dist/keywords/array-keywords.d.ts.map +1 -1
- package/dist/keywords/number-keywords.d.ts +2 -2
- package/dist/keywords/number-keywords.d.ts.map +1 -1
- package/dist/keywords/object-keywords.d.ts +2 -2
- package/dist/keywords/object-keywords.d.ts.map +1 -1
- package/dist/keywords/other-keywords.d.ts +2 -2
- package/dist/keywords/other-keywords.d.ts.map +1 -1
- package/dist/keywords/string-keywords.d.ts +2 -2
- package/dist/keywords/string-keywords.d.ts.map +1 -1
- package/dist/keywords.d.ts +2 -2
- package/dist/keywords.d.ts.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +23 -7
- package/dist/utils.d.ts.map +1 -1
- package/lib/formats.ts +225 -39
- package/lib/index.ts +199 -225
- package/lib/keywords/array-keywords.ts +138 -146
- package/lib/keywords/number-keywords.ts +78 -62
- package/lib/keywords/object-keywords.ts +199 -144
- package/lib/keywords/other-keywords.ts +137 -195
- package/lib/keywords/string-keywords.ts +24 -137
- package/lib/keywords.ts +2 -2
- package/lib/types.ts +37 -164
- package/lib/utils.ts +106 -28
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,11 +1,766 @@
|
|
|
1
|
-
|
|
1
|
+
# SchemaShield
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SchemaShield is a versatile and powerful JSON Schema validator designed to simplify the process of validating complex data structures.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Unlike many other libraries, SchemaShield does not rely on code generation, making it safer to pass real references to objects, classes, or variables and opening new possibilities for custom validation that are not possible with other libraries.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Despite its feature-rich and easy extendable nature, SchemaShield is designed to be fast and efficient, matching the performance of other libraries that use code generation.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Table of Contents
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
- [SchemaShield](#schemashield)
|
|
12
|
+
- [Table of Contents](#table-of-contents)
|
|
13
|
+
- [Features](#features)
|
|
14
|
+
- [Usage](#usage)
|
|
15
|
+
- [No Code Generation](#no-code-generation)
|
|
16
|
+
- [Error Handling](#error-handling)
|
|
17
|
+
- [ValidationError Properties](#validationerror-properties)
|
|
18
|
+
- [Get the cause of the error](#get-the-cause-of-the-error)
|
|
19
|
+
- [Adding Custom Types](#adding-custom-types)
|
|
20
|
+
- [Method Signature](#method-signature)
|
|
21
|
+
- [Example: Adding a Custom Type](#example-adding-a-custom-type)
|
|
22
|
+
- [Adding Custom Formats](#adding-custom-formats)
|
|
23
|
+
- [Method Signature](#method-signature-1)
|
|
24
|
+
- [Example: Adding a Custom Format](#example-adding-a-custom-format)
|
|
25
|
+
- [Adding Custom Keywords](#adding-custom-keywords)
|
|
26
|
+
- [Method Signature](#method-signature-2)
|
|
27
|
+
- [About the `defineError` Function](#about-the-defineerror-function)
|
|
28
|
+
- [About the `instance` Argument](#about-the-instance-argument)
|
|
29
|
+
- [Example: Adding a Custom Keyword](#example-adding-a-custom-keyword)
|
|
30
|
+
- [Complex example: Adding a Custom Keyword that uses the instance](#complex-example-adding-a-custom-keyword-that-uses-the-instance)
|
|
31
|
+
- [No Code Generation Opened Possibilities](#no-code-generation-opened-possibilities)
|
|
32
|
+
- [Immutable Mode](#immutable-mode)
|
|
33
|
+
- [TypeScript Support](#typescript-support)
|
|
34
|
+
- [Known Limitations](#known-limitations)
|
|
35
|
+
- [Schema References and Schema Definitions](#schema-references-and-schema-definitions)
|
|
36
|
+
- [Unsupported Formats](#unsupported-formats)
|
|
37
|
+
- [Internationalized Formats](#internationalized-formats)
|
|
38
|
+
- [Testing](#testing)
|
|
39
|
+
- [Contribute](#contribute)
|
|
40
|
+
- [Legal](#legal)
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- Supports draft-06 and draft-07 of the [JSON Schema](https://json-schema.org/) specification.
|
|
45
|
+
- No Code Generation for Enhanced Safety and Validation Flexibility.
|
|
46
|
+
- Custom type, keyword, and format validators.
|
|
47
|
+
- Immutable mode for data protection.
|
|
48
|
+
- Lightweight and fast.
|
|
49
|
+
- Easy to use and extend.
|
|
50
|
+
- Typescript support.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
**1. Install the package**
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install schema-shield
|
|
58
|
+
# or
|
|
59
|
+
yarn add schema-shield
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**2. Import the SchemaShield class**
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
import { SchemaShield } from "schema-shield";
|
|
66
|
+
// or
|
|
67
|
+
const { SchemaShield } = require("schema-shield");
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**3. Instantiate the SchemaShield class**
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
const schemaShield = new SchemaShield({ immutable: true });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**`immutable`** (optional): Set to `true` to ensure that input data remains unmodified during validation. Default is `false` for better performance.
|
|
77
|
+
|
|
78
|
+
**3.5. Add custom types, keywords, and formats (optional)**
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
schemaShield.addType("customType", (data) => {
|
|
82
|
+
// Custom type validation logic
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
schemaShield.addFormat("customFormat", (data) => {
|
|
86
|
+
// Custom format validation logic
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
schemaShield.addKeyword(
|
|
90
|
+
"customKeyword",
|
|
91
|
+
(schema, data, defineError, instance) => {
|
|
92
|
+
// Custom keyword validation logic
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**4. Compile a schema**
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
const schema = {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
name: { type: "string" },
|
|
104
|
+
age: { type: "number" }
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const validator = schemaShield.compile(schema);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**5. Validate some data**
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
const data = {
|
|
115
|
+
name: "John Doe",
|
|
116
|
+
age: 30
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const validationResult = validator(data);
|
|
120
|
+
|
|
121
|
+
if (validationResult.valid) {
|
|
122
|
+
console.log("Data is valid:", validationResult.data);
|
|
123
|
+
} else {
|
|
124
|
+
console.error("Validation error:", validationResult.error);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**`validationResult`**: Contains the following properties:
|
|
129
|
+
|
|
130
|
+
- `data`: The validated (and potentially modified) data
|
|
131
|
+
- `error`: A `ValidationError` instance if validation failed, otherwise null
|
|
132
|
+
- `valid`: true if validation was successful, otherwise false
|
|
133
|
+
|
|
134
|
+
## No Code Generation
|
|
135
|
+
|
|
136
|
+
Unlike some other validation libraries that rely on code generation to achieve fast performance, SchemaShield does not use code generation.
|
|
137
|
+
|
|
138
|
+
This design decision ensures that you can safely pass real references to objects, classes, or variables in your custom validation functions without any unintended side effects or security concerns.
|
|
139
|
+
|
|
140
|
+
For example, you can easily use `instanceof` to check if the provided data is an instance of a particular class or a subclass:
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
schemaShield.addType("date-class", (data) => data instanceof Date);
|
|
144
|
+
// or use your custom classes, functions or references
|
|
145
|
+
class CustomDate extends Date {}
|
|
146
|
+
schemaShield.addType("custom-date-class", (data) => data instanceof CustomDate);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
You can see a full example of this in the [No Code Generation opened possibilities](#no-code-generation-opened-possibilities) section.
|
|
150
|
+
|
|
151
|
+
## Error Handling
|
|
152
|
+
|
|
153
|
+
SchemaShield provides a `ValidationError` class to handle errors that occur during schema validation. When a validation error is encountered, a `ValidationError` instance is returned in the error property of the validation result.
|
|
154
|
+
|
|
155
|
+
This returned error instance uses the new [Error: cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) property introduced in ES6. This allows you to analyze the whole error chain or to retrieve the root cause of the error using the `getCause()` method.
|
|
156
|
+
|
|
157
|
+
### ValidationError Properties
|
|
158
|
+
|
|
159
|
+
- `message`: A string containing a description of the error
|
|
160
|
+
- `item`: The final item in the path that caused the error
|
|
161
|
+
- `keyword`: The keyword that triggered the error
|
|
162
|
+
- `cause`: A nested ValidationError that caused the current error
|
|
163
|
+
- `path`: The JSON Pointer path to the error location in the schema (Only available using the `getCause()` method)
|
|
164
|
+
- `data`: The data that caused the error (optional)
|
|
165
|
+
- `schema`: The compiled schema that caused the error (optional)
|
|
166
|
+
|
|
167
|
+
### Get the cause of the error
|
|
168
|
+
|
|
169
|
+
You can use the `getCause()` method to retrieve the root cause of a validation error. This method returns the nested ValidationError instance that triggered the current error and contains the `path` property.
|
|
170
|
+
|
|
171
|
+
**Example:**
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
import { SchemaShield } from "schema-shield";
|
|
175
|
+
|
|
176
|
+
const schemaShield = new SchemaShield({ immutable: true });
|
|
177
|
+
|
|
178
|
+
const schema = {
|
|
179
|
+
type: "object",
|
|
180
|
+
properties: {
|
|
181
|
+
name: { type: "string" },
|
|
182
|
+
age: {
|
|
183
|
+
type: "number",
|
|
184
|
+
minimum: 18
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const validator = schemaShield.compile(schema);
|
|
190
|
+
|
|
191
|
+
const invalidData = {
|
|
192
|
+
name: "John Doe",
|
|
193
|
+
age: 15
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const validationResult = validator(invalidData);
|
|
197
|
+
|
|
198
|
+
if (validationResult.valid) {
|
|
199
|
+
console.log("Data is valid:", validationResult.data);
|
|
200
|
+
} else {
|
|
201
|
+
console.error("Validation error:", validationResult.error.message); // "Property is invalid"
|
|
202
|
+
|
|
203
|
+
// Get the root cause of the error
|
|
204
|
+
const errorCause = validationResult.error.getCause();
|
|
205
|
+
console.error("Root cause:", errorCause.message); // "Value is less than the minimum"
|
|
206
|
+
console.error("Error path:", errorCause.path); // "#/properties/age/minimum"
|
|
207
|
+
console.error("Error data:", errorCause.data); // 15
|
|
208
|
+
console.error("Error schema:", errorCause.schema); // 18
|
|
209
|
+
console.error("Error keyword:", errorCause.keyword); // "minimum"
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Adding Custom Types
|
|
214
|
+
|
|
215
|
+
SchemaShield allows you to add custom types for validation using the `addType` method.
|
|
216
|
+
|
|
217
|
+
### Method Signature
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
interface TypeFunction {
|
|
221
|
+
(data: any): boolean;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
addType(name: string, validator: TypeFunction, overwrite?: boolean): void;
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- `name`: The name of the custom type. This should be a unique string that does not conflict with existing types.
|
|
228
|
+
- `validator`: A `TypeFunction` that takes a single argument `data` and returns a boolean value. The function should return `true` if the provided data is valid for the custom type, and `false` otherwise.
|
|
229
|
+
- `overwrite` (optional): Set to `true` to overwrite an existing type with the same name. Default is `false`. If set to `false` and a type with the same name already exists, an error will be thrown.
|
|
230
|
+
|
|
231
|
+
### Example: Adding a Custom Type
|
|
232
|
+
|
|
233
|
+
In this example, we'll add a custom type called age that validates if a given number is between 18 and 120.
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
import { SchemaShield } from "schema-shield";
|
|
237
|
+
|
|
238
|
+
const schemaShield = new SchemaShield();
|
|
239
|
+
|
|
240
|
+
// Custom type 'age' validator function
|
|
241
|
+
const ageValidator = (data) => {
|
|
242
|
+
return typeof data === "number" && data >= 18 && data <= 120;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Adding the custom type 'age'
|
|
246
|
+
schemaShield.addType("age", ageValidator);
|
|
247
|
+
|
|
248
|
+
const schema = {
|
|
249
|
+
type: "object",
|
|
250
|
+
properties: {
|
|
251
|
+
name: { type: "string" },
|
|
252
|
+
age: { type: "age" }
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const validator = schemaShield.compile(schema);
|
|
257
|
+
|
|
258
|
+
const validData = {
|
|
259
|
+
name: "John Doe",
|
|
260
|
+
age: 25
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const validationResult = validator(validData);
|
|
264
|
+
|
|
265
|
+
if (validationResult.valid) {
|
|
266
|
+
console.log("Data is valid:", validationResult.data);
|
|
267
|
+
} else {
|
|
268
|
+
console.error("Validation error:", validationResult.error.getCause().message);
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Adding Custom Formats
|
|
273
|
+
|
|
274
|
+
SchemaShield allows you to add custom formats for validation using the `addFormat` method.
|
|
275
|
+
|
|
276
|
+
### Method Signature
|
|
277
|
+
|
|
278
|
+
```javascript
|
|
279
|
+
interface FormatFunction {
|
|
280
|
+
(data: any): boolean;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
addFormat(name: string, validator: FormatFunction, overwrite?: boolean): void;
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- `name`: The name of the custom format. This should be a unique string that does not conflict with existing formats.
|
|
287
|
+
- `validator`: A FormatFunction that takes a single argument data and returns a boolean value. The function should return true if the provided data is valid for the custom format, and false otherwise.
|
|
288
|
+
- `overwrite` (optional): Set to true to overwrite an existing format with the same name. Default is false. If set to false and a format with the same name already exists, an error will be thrown.
|
|
289
|
+
|
|
290
|
+
### Example: Adding a Custom Format
|
|
291
|
+
|
|
292
|
+
In this example, we'll add a custom format called ssn that validates if a given string is a valid U.S. Social Security Number (SSN).
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
import { SchemaShield } from "./path/to/SchemaShield";
|
|
296
|
+
|
|
297
|
+
const schemaShield = new SchemaShield();
|
|
298
|
+
|
|
299
|
+
// Custom format 'ssn' validator function
|
|
300
|
+
const ssnValidator = (data) => {
|
|
301
|
+
const ssnPattern = /^(?!000|.+0{4})(?:\d{9}|\d{3}-\d{2}-\d{4})$/;
|
|
302
|
+
return typeof data === "string" && ssnPattern.test(data);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Adding the custom format 'ssn'
|
|
306
|
+
schemaShield.addFormat("ssn", ssnValidator);
|
|
307
|
+
|
|
308
|
+
const schema = {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
name: { type: "string" },
|
|
312
|
+
ssn: { type: "string", format: "ssn" }
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const validator = schemaShield.compile(schema);
|
|
317
|
+
|
|
318
|
+
const validData = {
|
|
319
|
+
name: "John Doe",
|
|
320
|
+
ssn: "123-45-6789"
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const validationResult = validator(validData);
|
|
324
|
+
|
|
325
|
+
if (validationResult.valid) {
|
|
326
|
+
console.log("Data is valid:", validationResult.data);
|
|
327
|
+
} else {
|
|
328
|
+
console.error("Validation error:", validationResult.error.getCause().message);
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Adding Custom Keywords
|
|
333
|
+
|
|
334
|
+
SchemaShield allows you to add custom keywords for validation using the addKeyword method. This is the most powerful method for adding custom validation logic to SchemaShield because it allows to interact with the entire schema and data being validated at the level of the keyword.
|
|
335
|
+
|
|
336
|
+
### Method Signature
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
type Result = void | ValidationError;
|
|
340
|
+
|
|
341
|
+
interface DefineErrorOptions {
|
|
342
|
+
item?: any; // Final item in the path
|
|
343
|
+
cause?: ValidationError; // Cause of the error
|
|
344
|
+
data?: any; // Data that caused the error
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
interface DefineErrorFunction {
|
|
348
|
+
(message: string, options?: DefineErrorOptions): ValidationError;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
interface ValidateFunction {
|
|
352
|
+
(data: any): Result;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
interface CompiledSchema {
|
|
356
|
+
$validate?: ValidateFunction;
|
|
357
|
+
[key: string]: any;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
interface FormatFunction {
|
|
361
|
+
(data: any): boolean;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
interface TypeFunction {
|
|
365
|
+
(data: any): boolean;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
declare class SchemaShield {
|
|
369
|
+
constructor({ immutable }?: {
|
|
370
|
+
immutable?: boolean;
|
|
371
|
+
});
|
|
372
|
+
compile(schema: any): Validator;
|
|
373
|
+
addType(name: string, validator: TypeFunction, overwrite?: boolean): void;
|
|
374
|
+
addFormat(name: string, validator: FormatFunction, overwrite?: boolean): void;
|
|
375
|
+
addKeyword(name: string, validator: KeywordFunction, overwrite?: boolean): void;
|
|
376
|
+
getType(type: string): TypeFunction | false;
|
|
377
|
+
getFormat(format: string): FormatFunction | false;
|
|
378
|
+
getKeyword(keyword: string): KeywordFunction | false;
|
|
379
|
+
isSchemaLike(subSchema: any): boolean;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
interface KeywordFunction {
|
|
383
|
+
(
|
|
384
|
+
schema: CompiledSchema,
|
|
385
|
+
data: any,
|
|
386
|
+
defineError: DefineErrorFunction,
|
|
387
|
+
instance: SchemaShield
|
|
388
|
+
): Result;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
addKeyword(name: string, validator: KeywordFunction, overwrite?: boolean): void;
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
- `name`: The name of the custom keyword. This should be a unique string that does not conflict with existing keywords.
|
|
397
|
+
- `validator`: A `KeywordFunction` that takes four arguments: `schema`, `data`, `defineError`, and `instance` (The SchemaShield instance that is currently running the validation). The function should not return anything if the data is valid for the custom keyword, and should return a `ValidationError` instance if the data is invalid.
|
|
398
|
+
- `overwrite` (optional): Set to true to overwrite an existing keyword with the same name. Default is false. If set to false and a keyword with the same name already exists, an error will be thrown.
|
|
399
|
+
|
|
400
|
+
#### About the `defineError` Function
|
|
401
|
+
|
|
402
|
+
Take into account that the error must be generated using the `defineError` function because the error returned by this method has the required data relevant for the current keyword (`schema`, `keyword`, `getCause` method).
|
|
403
|
+
|
|
404
|
+
- `message`: A string that describes the validation error.
|
|
405
|
+
- `options`: An optional object with properties that provide more context for the error:
|
|
406
|
+
- `item`?: An optional value representing the final item in the path where the validation error occurred. (e.g. index of an array item)
|
|
407
|
+
- `cause`?: An optional `ValidationError` that represents the cause of the current error.
|
|
408
|
+
- `data`?: An optional value representing the data that caused the validation error.
|
|
409
|
+
|
|
410
|
+
#### About the `instance` Argument
|
|
411
|
+
|
|
412
|
+
The `instance` argument is the SchemaShield instance that is currently running the validation. This can be used to access to other `types`, `keywords` or `formats` that have been added to the instance.
|
|
413
|
+
|
|
414
|
+
### Example: Adding a Custom Keyword
|
|
415
|
+
|
|
416
|
+
In this example, we'll add a custom keyword called divisibleBy that validates if a given number is divisible by a specified divisor.
|
|
417
|
+
|
|
418
|
+
```javascript
|
|
419
|
+
import { SchemaShield, ValidationError } from "./path/to/SchemaShield";
|
|
420
|
+
|
|
421
|
+
const schemaShield = new SchemaShield({ immutable: true });
|
|
422
|
+
|
|
423
|
+
// Custom keyword 'divisibleBy' validator function
|
|
424
|
+
const divisibleByValidator = (schema, data, defineError, instance) => {
|
|
425
|
+
if (typeof data !== "number") {
|
|
426
|
+
return defineError("Value must be a number", {
|
|
427
|
+
data
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (data % schema.divisibleBy !== 0) {
|
|
432
|
+
return defineError(`Value must be divisible by ${schema.divisibleBy}`, {
|
|
433
|
+
data
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// Adding the custom keyword 'divisibleBy'
|
|
439
|
+
schemaShield.addKeyword("divisibleBy", divisibleByValidator);
|
|
440
|
+
|
|
441
|
+
const schema = {
|
|
442
|
+
type: "object",
|
|
443
|
+
properties: {
|
|
444
|
+
value: { type: "number", divisibleBy: 5 }
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const validator = schemaShield.compile(schema);
|
|
449
|
+
|
|
450
|
+
const validData = {
|
|
451
|
+
value: 15
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const validationResult = validator(validData);
|
|
455
|
+
|
|
456
|
+
if (validationResult.valid) {
|
|
457
|
+
console.log("Data is valid:", validationResult.data);
|
|
458
|
+
} else {
|
|
459
|
+
console.error("Validation error:", validationResult.error.getCause().message);
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Complex example: Adding a Custom Keyword that uses the instance
|
|
464
|
+
|
|
465
|
+
In this example we'll add a custom keyword called `prefixedUsername` that will validate if a given string is a valid username and has a specific prefix. This will only work if the additional validation methods and types have been added to the instance.
|
|
466
|
+
|
|
467
|
+
```javascript
|
|
468
|
+
import { SchemaShield, ValidationError } from "schema-shield";
|
|
469
|
+
|
|
470
|
+
const schemaShield = new SchemaShield();
|
|
471
|
+
|
|
472
|
+
// Custom type validator: nonEmptyString
|
|
473
|
+
const nonEmptyStringValidator = (data) =>
|
|
474
|
+
typeof data === "string" && data.length > 0;
|
|
475
|
+
schemaShield.addType("nonEmptyString", nonEmptyStringValidator);
|
|
476
|
+
|
|
477
|
+
// Custom keyword validator: hasPrefix
|
|
478
|
+
const hasPrefixValidator = (schema, data, defineError, instance) => {
|
|
479
|
+
const { prefix } = schema.hasPrefix;
|
|
480
|
+
if (typeof data === "string" && !data.startsWith(prefix)) {
|
|
481
|
+
return defineError(`String must have the prefix "${prefix}"`, {
|
|
482
|
+
data
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
schemaShield.addKeyword("hasPrefix", hasPrefixValidator);
|
|
487
|
+
|
|
488
|
+
// Custom format validator: username
|
|
489
|
+
const usernameValidator = (data) => /^[a-zA-Z0-9._-]{3,}$/i.test(data);
|
|
490
|
+
schemaShield.addFormat("username", usernameValidator);
|
|
491
|
+
|
|
492
|
+
// Custom keyword 'prefixedUsername' validator function
|
|
493
|
+
const prefixedUsername = (schema, data, defineError, instance) => {
|
|
494
|
+
const { validType, prefixValidator, validFormat } = schema.prefixedUsername;
|
|
495
|
+
|
|
496
|
+
// Get the validators for the specified types and formats from the instance
|
|
497
|
+
// (if they exist)
|
|
498
|
+
const typeValidator = instance.getType(validType);
|
|
499
|
+
const prefixValidator = instance.getKeyword(prefixValidator);
|
|
500
|
+
const formatValidator = instance.getFormat(validFormat);
|
|
501
|
+
|
|
502
|
+
for (let i = 0; i < data.length; i++) {
|
|
503
|
+
const item = data[i];
|
|
504
|
+
|
|
505
|
+
// Validate that the data is of the correct type if specified
|
|
506
|
+
if (validType && typeValidator) {
|
|
507
|
+
if (!typeValidator(item)) {
|
|
508
|
+
return defineError(`Invalid type: ${validType}`, {
|
|
509
|
+
item: i,
|
|
510
|
+
data: data[i]
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Validate that the data has the correct format if specified
|
|
516
|
+
if (validFormat && formatValidator) {
|
|
517
|
+
if (!formatValidator(item)) {
|
|
518
|
+
return defineError(`Invalid format: ${validFormat}`, {
|
|
519
|
+
item: i,
|
|
520
|
+
data: data[i]
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Validate that the data has the correct prefix if specified
|
|
526
|
+
if (prefixValidator) {
|
|
527
|
+
const error = prefixValidator(schema, item, defineError, instance);
|
|
528
|
+
if (error) {
|
|
529
|
+
return defineError(`Invalid prefix: ${prefixValidator}`, {
|
|
530
|
+
cause: error,
|
|
531
|
+
item: i,
|
|
532
|
+
data: data[i]
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
schemaShield.addKeyword("prefixedUsername", prefixedUsername);
|
|
540
|
+
|
|
541
|
+
const schema = {
|
|
542
|
+
type: "array",
|
|
543
|
+
prefixedUsername: {
|
|
544
|
+
validType: "nonEmptyString",
|
|
545
|
+
prefixValidator: "hasPrefix",
|
|
546
|
+
validFormat: "username"
|
|
547
|
+
},
|
|
548
|
+
items: {
|
|
549
|
+
type: "string"
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const validator = schemaShield.compile(schema);
|
|
554
|
+
|
|
555
|
+
const validData = ["user.john", "user.jane"];
|
|
556
|
+
|
|
557
|
+
const validationResult = validator(validData);
|
|
558
|
+
|
|
559
|
+
if (validationResult.valid) {
|
|
560
|
+
console.log("Data is valid:", validationResult.data);
|
|
561
|
+
} else {
|
|
562
|
+
console.error("Validation error:", validationResult.error.getCause().message);
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## No Code Generation Opened Possibilities
|
|
567
|
+
|
|
568
|
+
With the no code generation nature of SchemaShield, you can create complex validation logic that incorporates custom classes, objects, or variables. This flexibility allows you to seamlessly integrate the validation process into your application's unique requirements and data structures.
|
|
569
|
+
|
|
570
|
+
For example, imagine you have a custom class representing a project and another representing an employee. You could create a custom validator to ensure that only employees with the right qualifications are assigned to a specific project:
|
|
571
|
+
|
|
572
|
+
```javascript
|
|
573
|
+
import { SchemaShield, ValidationError } from "schema-shield";
|
|
574
|
+
|
|
575
|
+
const schemaShield = new SchemaShield();
|
|
576
|
+
|
|
577
|
+
// Custom classes
|
|
578
|
+
class Project {
|
|
579
|
+
constructor(name: string, requiredSkills: string[]) {
|
|
580
|
+
this.name = name;
|
|
581
|
+
this.requiredSkills = requiredSkills;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
class Employee {
|
|
586
|
+
constructor(name: string, skills: string[]) {
|
|
587
|
+
this.name = name;
|
|
588
|
+
this.skills = skills;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
hasSkillsForProject(project: Project) {
|
|
592
|
+
return project.requiredSkills.every((skill) => this.skills.includes(skill));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Add custom types to the instance
|
|
597
|
+
schemaShield.addType("project", (data) => data instanceof Project);
|
|
598
|
+
schemaShield.addType("employee", (data) => data instanceof Employee);
|
|
599
|
+
|
|
600
|
+
schemaShield.addKeyword(
|
|
601
|
+
"requiresQualifiedEmployee",
|
|
602
|
+
(schema, data, defineError, instance) => {
|
|
603
|
+
const { assignment, project, employee } = data;
|
|
604
|
+
|
|
605
|
+
const stringTypeValidator = instance.getType("string");
|
|
606
|
+
const projectTypeValidator = instance.getType("project");
|
|
607
|
+
const employeeTypeValidator = instance.getType("employee");
|
|
608
|
+
|
|
609
|
+
if (!stringTypeValidator(assignment)) {
|
|
610
|
+
return defineError("Assignment must be a string", {
|
|
611
|
+
item: "assignment",
|
|
612
|
+
data: assignment
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (!projectTypeValidator(project)) {
|
|
617
|
+
return defineError("Project must be a Project instance", {
|
|
618
|
+
item: "project",
|
|
619
|
+
data: project
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!employeeTypeValidator(employee)) {
|
|
624
|
+
return defineError("Employee must be an Employee instance", {
|
|
625
|
+
item: "employee",
|
|
626
|
+
data: employee
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (schema.requiresQualifiedEmployee) {
|
|
631
|
+
if (!employee.hasSkillsForProject(project)) {
|
|
632
|
+
return defineError(
|
|
633
|
+
"Employee does not meet the project's requirements",
|
|
634
|
+
{
|
|
635
|
+
data: {
|
|
636
|
+
assignment,
|
|
637
|
+
project,
|
|
638
|
+
employee
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
// Create and compile the schema
|
|
648
|
+
const schema = {
|
|
649
|
+
type: "object",
|
|
650
|
+
properties: {
|
|
651
|
+
assignment: {}, // Empty schema because we will validate it with the custom keyword
|
|
652
|
+
project: {}, // Empty schema because we will validate it with the custom keyword
|
|
653
|
+
employee: {} // Empty schema because we will validate it with the custom keyword
|
|
654
|
+
},
|
|
655
|
+
required: ["assignment", "project", "employee"],
|
|
656
|
+
requiresQualifiedEmployee: true
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const validator = schemaShield.compile(schema);
|
|
660
|
+
|
|
661
|
+
// Create some data to validate
|
|
662
|
+
const employee1 = new Employee("Employee 1", ["A", "B", "C"]);
|
|
663
|
+
|
|
664
|
+
const project1 = new Project("Project 1", ["A", "B"]);
|
|
665
|
+
|
|
666
|
+
const dataToValidate = {
|
|
667
|
+
assignment: "Assignment 1 for Project 1",
|
|
668
|
+
project: project1,
|
|
669
|
+
employee: employee1
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// Validate the data
|
|
673
|
+
const validationResult = validator(schema);
|
|
674
|
+
|
|
675
|
+
if (validationResult.valid) {
|
|
676
|
+
console.log("Assignment is valid:", validationResult.data);
|
|
677
|
+
} else {
|
|
678
|
+
console.error("Validation error:", validationResult.error.message);
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
In this example, SchemaShield safely accesses instances of custom classes and utilizes them in the validation process. This level of complexity and flexibility would not be possible or would require a lot of boilerplate code with other libraries that rely on code generation.
|
|
683
|
+
|
|
684
|
+
## Immutable Mode
|
|
685
|
+
|
|
686
|
+
SchemaShield offers an optional immutable mode to prevent modifications to the input data during validation. In some cases, SchemaShield may mutate the data when using the `default` keyword or within custom added keywords.
|
|
687
|
+
|
|
688
|
+
By enabling immutable mode, the library creates a deep copy of the input data before performing any validation checks, ensuring that the original data remains unchanged throughout the process. This feature can be useful in situations where preserving the integrity of the input data is essential.
|
|
689
|
+
|
|
690
|
+
To enable immutable mode, simply pass the `immutable` option when creating a new `SchemaShield` instance:
|
|
691
|
+
|
|
692
|
+
```javascript
|
|
693
|
+
const schemaShield = new SchemaShield({ immutable: true });
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
By default, the immutable mode is disabled. If you don't need the immutability guarantee, you can leave it disabled to optimize performance.
|
|
697
|
+
|
|
698
|
+
However, there are some caveats to consider when using immutable mode. The deep copy may not accurately reproduce complex objects such as instantiated classes. In such cases, you can handle the cloning process yourself using a custom keyword to ensure the proper preservation of your data's structure.
|
|
699
|
+
|
|
700
|
+
## TypeScript Support
|
|
701
|
+
|
|
702
|
+
SchemaShield offers comprehensive TypeScript support, enhancing the library's usability for TypeScript projects. Type definitions are included in the package, so you can import the library and use it in your TypeScript projects without any additional configuration.
|
|
703
|
+
|
|
704
|
+
With the built in TypeScript support, you can take advantage of features like strong typing, autocompletion, and compile-time error checking, which can help you catch potential issues early and improve the overall quality of your code.
|
|
705
|
+
|
|
706
|
+
## Known Limitations
|
|
707
|
+
|
|
708
|
+
SchemaShield is a powerful and flexible library, but there are some limitations to be aware of when using it. Some features are not yet supported in the current version.
|
|
709
|
+
|
|
710
|
+
### Schema References and Schema Definitions
|
|
711
|
+
|
|
712
|
+
SchemaShield currently does not support schema references and schema definitions (i.e. `$ref` and `definitions`). This is planned to be addressed in future updates of SchemaShield.
|
|
713
|
+
|
|
714
|
+
For now, consider using custom implementations using the `addKeyword` method or use alternative libraries to handle these specific features if you need them.
|
|
715
|
+
|
|
716
|
+
### Unsupported Formats
|
|
717
|
+
|
|
718
|
+
#### Internationalized Formats
|
|
719
|
+
|
|
720
|
+
There is no plan to support the following formats in SchemaShield, as they are not relevant to the majority of use cases. If you need to use these formats, consider using custom implementations using the `addFormat` method to handle them.
|
|
721
|
+
|
|
722
|
+
- `idn-email`
|
|
723
|
+
- `idn-hostname`
|
|
724
|
+
- `iri`
|
|
725
|
+
- `iri-reference`
|
|
726
|
+
|
|
727
|
+
Also you can contribute to SchemaShield and add support for these keywords and formats or leve a comment requesting support for them.
|
|
728
|
+
|
|
729
|
+
## Testing
|
|
730
|
+
|
|
731
|
+
SchemaShield prioritizes reliability and accuracy in JSON Schema validation by using the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
|
|
732
|
+
|
|
733
|
+
This comprehensive test suite ensures compliance with the JSON Schema standard, providing developers with a dependable and consistent validation experience.
|
|
734
|
+
|
|
735
|
+
```bash
|
|
736
|
+
npm test
|
|
737
|
+
# or
|
|
738
|
+
npm run test:dev # for development
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
## Contribute
|
|
742
|
+
|
|
743
|
+
SchemaShield is an open-source project, and we welcome contributions from the community. By contributing to SchemaShield, you can help improve the library and expand its feature set.
|
|
744
|
+
|
|
745
|
+
If you are interested in contributing, please follow these steps:
|
|
746
|
+
|
|
747
|
+
- **Fork the repository:** Fork the SchemaShield repository on GitHub and clone it to your local machine.
|
|
748
|
+
|
|
749
|
+
- **Create a feature branch:** Create a new branch for your feature or bugfix. Make sure to give it a descriptive name.
|
|
750
|
+
|
|
751
|
+
- **Implement your changes:** Make the necessary changes to the codebase. Be sure to add or update the relevant tests and documentation.
|
|
752
|
+
|
|
753
|
+
- **Test your changes:** Before submitting your pull request, make sure your changes pass all existing tests and any new tests you've added. It's also a good idea to ensure that your changes do not introduce any performance regressions or new issues.
|
|
754
|
+
|
|
755
|
+
- **Submit a pull request:** Once your changes are complete and tested, submit a pull request to the main SchemaShield repository. In your pull request description, please provide a brief summary of your changes and any relevant context.
|
|
756
|
+
|
|
757
|
+
- **Code review:** Your pull request will be reviewed and may request changes or provide feedback. Be prepared to engage in a discussion and possibly make further changes to your code based on the feedback.
|
|
758
|
+
|
|
759
|
+
- **Merge:** Once your pull request is approved, it will be merged into the main SchemaShield repository.
|
|
760
|
+
|
|
761
|
+
We appreciate your interest in contributing to SchemaShield and look forward to your valuable input. Together, we can make SchemaShield an even better library for the community.
|
|
762
|
+
|
|
763
|
+
## Legal
|
|
764
|
+
|
|
765
|
+
Author: [Masquerade Circus](http://masquerade-circus.net).
|
|
766
|
+
License [Apache-2.0](https://opensource.org/licenses/Apache-2.0)
|