schema-shield 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +400 -1050
- package/dist/formats.d.ts.map +1 -1
- package/dist/index.d.ts +33 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1247 -392
- package/dist/index.min.js +2 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.mjs +1247 -392
- package/dist/keywords/array-keywords.d.ts.map +1 -1
- package/dist/keywords/number-keywords.d.ts.map +1 -1
- package/dist/keywords/object-keywords.d.ts +7 -1
- package/dist/keywords/object-keywords.d.ts.map +1 -1
- package/dist/keywords/other-keywords.d.ts +17 -1
- package/dist/keywords/other-keywords.d.ts.map +1 -1
- package/dist/keywords/string-keywords.d.ts.map +1 -1
- package/dist/utils/deep-freeze.d.ts.map +1 -1
- package/dist/utils/main-utils.d.ts +7 -4
- package/dist/utils/main-utils.d.ts.map +1 -1
- package/lib/formats.ts +36 -10
- package/lib/index.ts +1157 -155
- package/lib/keywords/array-keywords.ts +18 -3
- package/lib/keywords/number-keywords.ts +19 -5
- package/lib/keywords/object-keywords.ts +59 -61
- package/lib/keywords/other-keywords.ts +205 -192
- package/lib/keywords/string-keywords.ts +51 -8
- package/lib/types.ts +2 -2
- package/lib/utils/deep-freeze.ts +6 -4
- package/lib/utils/main-utils.ts +91 -47
- package/package.json +8 -10
package/README.md
CHANGED
|
@@ -1,248 +1,256 @@
|
|
|
1
1
|
# SchemaShield 🛡️
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Secure, Stack-Safe, Offline, and Domain-Aware JSON Schema Validation.**
|
|
4
4
|
|
|
5
|
-
SchemaShield is a
|
|
5
|
+
SchemaShield is a high-performance JSON Schema interpreter for JavaScript applications that need predictable validation without runtime code generation or hidden network access.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
In the latest benchmark run, SchemaShield reached approximately **80% of AJV's throughput** and ran approximately **15% faster than schemasafe**.
|
|
8
|
+
|
|
9
|
+
- No `eval()` or `new Function()`
|
|
10
|
+
- Synchronous, offline validation
|
|
11
|
+
- No implicit fetch, HTTP, DNS, or network I/O
|
|
12
|
+
- First-class validation for JavaScript objects and domain rules
|
|
13
|
+
- Structured error paths, causes, and trees
|
|
14
|
+
- Zero runtime dependencies
|
|
15
|
+
- Verified CJS, ESM, browser, and TypeScript declaration packages
|
|
10
16
|
|
|
11
17
|
## Quick Start
|
|
12
18
|
|
|
19
|
+
Install SchemaShield:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install schema-shield
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or with Bun:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bun add schema-shield
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Compile a schema and reuse the validator:
|
|
32
|
+
|
|
13
33
|
```javascript
|
|
14
34
|
import { SchemaShield } from "schema-shield";
|
|
15
35
|
|
|
16
|
-
const
|
|
36
|
+
const validateUser = new SchemaShield({ failFast: false }).compile({
|
|
17
37
|
type: "object",
|
|
18
38
|
properties: {
|
|
19
|
-
name: { type: "string" },
|
|
20
|
-
age: { type: "number" }
|
|
21
|
-
}
|
|
39
|
+
name: { type: "string", minLength: 1 },
|
|
40
|
+
age: { type: "number", minimum: 18 }
|
|
41
|
+
},
|
|
42
|
+
required: ["name", "age"],
|
|
43
|
+
additionalProperties: false
|
|
22
44
|
});
|
|
23
45
|
|
|
24
|
-
|
|
25
|
-
// { valid: true, data: { name: "
|
|
46
|
+
const validResult = validateUser({ name: "Ada", age: 37 });
|
|
47
|
+
// { valid: true, data: { name: "Ada", age: 37 }, error: null }
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
// { valid: false, error: ValidationError }
|
|
49
|
+
const invalidResult = validateUser({ name: "Ada", age: 16 });
|
|
50
|
+
// { valid: false, data: { name: "Ada", age: 16 }, error: ValidationError }
|
|
29
51
|
```
|
|
30
52
|
|
|
31
|
-
|
|
53
|
+
> The default `failFast: true` mode returns `error: true` when validation fails. With `failFast: false`, built-in failures and custom keyword failures created with `defineError()` return a detailed `ValidationError` with paths, causes, and an error tree. A custom keyword that returns `true` directly still produces `error: true`. Use `defineError()` for detailed errors.
|
|
54
|
+
|
|
55
|
+
## Contents
|
|
32
56
|
|
|
33
|
-
- [Quick Start](#quick-start)
|
|
34
57
|
- [Why SchemaShield?](#why-schemashield)
|
|
35
|
-
|
|
36
|
-
- [Usage](#usage)
|
|
58
|
+
- [Architecture Comparison](#architecture-comparison)
|
|
37
59
|
- [Performance](#performance)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- [
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
44
|
-
|
|
45
|
-
- [
|
|
46
|
-
- [Error Handling](#error-handling)
|
|
47
|
-
- [Adding Custom Types](#adding-custom-types)
|
|
48
|
-
- [Method Signature](#method-signature)
|
|
49
|
-
- [Example: Adding a Custom Type](#example-adding-a-custom-type)
|
|
50
|
-
- [Adding Custom Formats](#adding-custom-formats)
|
|
51
|
-
- [Method Signature](#method-signature-1)
|
|
52
|
-
- [Example: Adding a Custom Format](#example-adding-a-custom-format)
|
|
53
|
-
- [Adding Custom Keywords](#adding-custom-keywords)
|
|
54
|
-
- [Method Signature](#method-signature-2)
|
|
55
|
-
- [Example: Adding a Custom Keyword](#example-adding-a-custom-keyword)
|
|
56
|
-
- [Complex example: Adding a Custom Keyword that uses the instance](#complex-example-adding-a-custom-keyword-that-uses-the-instance)
|
|
57
|
-
- [Supported Formats](#supported-formats)
|
|
58
|
-
- [Validating Runtime Objects](#validating-runtime-objects)
|
|
59
|
-
- [More on Error Handling](#more-on-error-handling)
|
|
60
|
-
- [ValidationError Properties](#validationerror-properties)
|
|
61
|
-
- [Get the path to the error location](#get-the-path-to-the-error-location)
|
|
62
|
-
- [Get the full error chain as a tree](#get-the-full-error-chain-as-a-tree)
|
|
63
|
-
- [Get the cause of the error](#get-the-cause-of-the-error)
|
|
64
|
-
- [Immutable Mode](#immutable-mode)
|
|
65
|
-
- [TypeScript Support](#typescript-support)
|
|
66
|
-
- [Known Limitations](#known-limitations)
|
|
67
|
-
- [1. Dynamic ID Scope Resolution (Scope Alteration)](#1-dynamic-id-scope-resolution-scope-alteration)
|
|
68
|
-
- [2. Unicode Length Validation](#2-unicode-length-validation)
|
|
69
|
-
- [3. Conservative Equality Path (Performance Trade-off)](#3-conservative-equality-path-performance-trade-off)
|
|
60
|
+
- [Offline by Design](#offline-by-design)
|
|
61
|
+
- [Domain-Aware Validation](#domain-aware-validation)
|
|
62
|
+
- [Edge, Serverless, and Package Support](#edge-serverless-and-package-support)
|
|
63
|
+
- [Core API](#core-api)
|
|
64
|
+
- [Errors and Debugging](#errors-and-debugging)
|
|
65
|
+
- [Extensibility](#extensibility)
|
|
66
|
+
- [Supported Formats and Compatibility](#supported-formats-and-compatibility)
|
|
67
|
+
- [Immutable Mode and Known Limitations](#immutable-mode-and-known-limitations)
|
|
70
68
|
- [Testing](#testing)
|
|
71
69
|
- [Contribute](#contribute)
|
|
72
70
|
- [Legal](#legal)
|
|
73
71
|
|
|
74
72
|
## Why SchemaShield?
|
|
75
73
|
|
|
76
|
-
|
|
74
|
+
SchemaShield gives developers competitive performance without surrendering control of the validation runtime.
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
| :------------------- | :------------------------------------------------------------------- | :--------------------------------------------------------- | :-------------------------------------------------------------------------------------- |
|
|
80
|
-
| **Security Model** | **Reactive**<br>(Requires config to prevent Prototype Pollution/DoS) | **Preventive**<br>(Hermetic & Immutable by design) | "Secure by default" prevents human error and vulnerabilities in production. |
|
|
81
|
-
| **Debug Experience** | **Black Box**<br>(Debugs opaque generated strings/code) | **White Box**<br>(Standard JS stack traces) | Drastically reduces time-to-fix when validation fails in complex logic. |
|
|
82
|
-
| **Stability** | **Recursive**<br>(Risk of Stack Overflow on deep data) | **Flat Loop**<br>(Constant memory usage) | Protects your server availability against malicious deep-payload attacks. |
|
|
83
|
-
| **Domain Logic** | **Disconnected**<br>(Hard to validate Class Instances/State) | **Integrated**<br>(Native `instanceof` & state validation) | Unifies DTO validation and Business Rules, eliminating "spaghetti code" in controllers. |
|
|
84
|
-
| **Ecosystem** | **Fragmented**<br>(Requires plugins for errors/formats) | **Cohesive**<br>(Advanced error trees & formats built-in) | Reduces dependency fatigue and maintenance burden. |
|
|
76
|
+
JIT validators generate executable code to maximize throughput. SchemaShield uses an interpreter architecture that stays inside ordinary JavaScript execution, works in restrictive CSP environments, and keeps validation visible through standard stack traces and structured errors.
|
|
85
77
|
|
|
86
|
-
|
|
78
|
+
Choose SchemaShield when your application needs:
|
|
87
79
|
|
|
88
|
-
|
|
80
|
+
- **Secure-by-design execution:** No `eval()`, `new Function()`, or validator-initiated network access.
|
|
81
|
+
- **Competitive throughput:** In the latest benchmark run, SchemaShield reached approximately 80% of AJV's throughput and ran approximately 15% faster than schemasafe.
|
|
82
|
+
- **Stack-safe validation:** Schemas beyond the fixed compile-depth limit are rejected, while recursive validation paths that exceed `maxDepth` fail with a controlled error instead of a stack overflow.
|
|
83
|
+
- **Domain-aware rules:** Validate class instances, Dates, application state, and custom business logic through the same API.
|
|
84
|
+
- **Inspectable failures:** Follow schema paths, instance paths, root causes, and complete error trees.
|
|
85
|
+
- **A cohesive runtime:** Built-in formats, custom types, custom keywords, immutable mode, and zero runtime dependencies.
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
| :---------------------------- | :-------------------------------------------------- | :------------------------------- | :-------------------- |
|
|
92
|
-
| **Architecture** | **Secure Flat Interpreter** | JIT Compiler (eval/new Function) | Recursive Interpreter |
|
|
93
|
-
| **Relative Speed** | **High (~70%)** | Reference (100%) | Low (1% - 20%) |
|
|
94
|
-
| **CSP Compliance** | **Native (100% Safe)** | Requires Build Config | Variable |
|
|
95
|
-
| **Edge Ready** | **Native** | Complex Setup | Variable |
|
|
96
|
-
| **Stack Safety** | **Minimized stack usage (non-recursive core loop)** | Risk of Overflow | Risk of Overflow |
|
|
97
|
-
| **Class Instance Validation** | **Native** | No | No |
|
|
98
|
-
| **Debug Experience** | **Clean Stack Trace** | Opaque Generated Code | Variable |
|
|
87
|
+
## Architecture Comparison
|
|
99
88
|
|
|
100
|
-
|
|
89
|
+
SchemaShield was designed for developers who want strong performance, predictable execution, and direct integration with real application domains.
|
|
101
90
|
|
|
102
|
-
|
|
91
|
+
| Capability | SchemaShield | JIT / Codegen Validators | Conventional Interpreters |
|
|
92
|
+
| :------------------------------ | :-------------------------------------------------------------------- | :----------------------------------------- | :-------------------------------------- |
|
|
93
|
+
| **Runtime execution** | Interpreter without runtime code generation | Generated executable code | Interpreted validation |
|
|
94
|
+
| **`eval()` / `new Function()`** | Not used | Common to the codegen model | Usually not required |
|
|
95
|
+
| **Network behavior** | Offline with no implicit network access | Depends on library and configuration | Varies |
|
|
96
|
+
| **Stack behavior** | Controlled compile and runtime depth errors instead of stack overflow | Depends on generated execution | Recursive traversal is common |
|
|
97
|
+
| **Debugging** | Standard JavaScript stacks and structured error trees | Generated code can obscure validation flow | Varies |
|
|
98
|
+
| **Domain integration** | Class instances, runtime objects, and custom business rules | Extension model varies | Usually requires additional integration |
|
|
99
|
+
| **Extensibility** | Types, formats, keywords, and error tooling in one API | Frequently configuration or plugin driven | Varies |
|
|
103
100
|
|
|
104
|
-
|
|
101
|
+
## Performance
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
npm install schema-shield
|
|
108
|
-
# or
|
|
109
|
-
bun add schema-shield
|
|
110
|
-
```
|
|
103
|
+
SchemaShield delivers high-performance interpreted validation, offline execution, stack safety, standard JavaScript debugging, and domain-aware extensibility without runtime code generation.
|
|
111
104
|
|
|
112
|
-
|
|
105
|
+
In the latest benchmark run:
|
|
113
106
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// or
|
|
117
|
-
const { SchemaShield } = require("schema-shield");
|
|
118
|
-
```
|
|
107
|
+
- SchemaShield reached approximately **80% of AJV's throughput**.
|
|
108
|
+
- SchemaShield ran approximately **15% faster than schemasafe**.
|
|
119
109
|
|
|
120
|
-
|
|
110
|
+
These results position SchemaShield as a the second fastest interpreter.
|
|
121
111
|
|
|
122
|
-
|
|
123
|
-
const schemaShield = new SchemaShield();
|
|
124
|
-
```
|
|
112
|
+
> Benchmark results may vary by runtime, hardware, schema mix, and validator configuration. The benchmark results shown remain consistent across multiple runs on different environments.
|
|
125
113
|
|
|
126
|
-
|
|
127
|
-
- **`failFast`** (optional): Set to `false` to receive detailed error objects on validation failure. Default is `true` for lightweight failure indication.
|
|
114
|
+
## Offline by Design
|
|
128
115
|
|
|
129
|
-
|
|
116
|
+
SchemaShield keeps validation inside your application's trust boundary.
|
|
130
117
|
|
|
131
|
-
|
|
132
|
-
schemaShield.addType("customType", (data) => {
|
|
133
|
-
// Custom type validation logic
|
|
134
|
-
});
|
|
118
|
+
SchemaShield itself performs no fetches, HTTP requests, DNS lookups, or other network I/O during schema compilation or validation. Validation behavior ships with your application and does not depend on a remote schema host remaining available or unchanged.
|
|
135
119
|
|
|
136
|
-
|
|
137
|
-
// Custom format validation logic
|
|
138
|
-
});
|
|
120
|
+
### No Remote References by Design
|
|
139
121
|
|
|
140
|
-
|
|
141
|
-
"customKeyword",
|
|
142
|
-
(schema, data, defineError, instance) => {
|
|
143
|
-
// Custom keyword validation logic
|
|
144
|
-
}
|
|
145
|
-
);
|
|
146
|
-
```
|
|
122
|
+
Remote `$ref` fetching is intentionally excluded from SchemaShield.
|
|
147
123
|
|
|
148
|
-
|
|
124
|
+
A validator that retrieves schemas at runtime introduces a network-capable component into the validation path. That expands the attack surface, creates an SSRF vector, adds DNS and availability dependencies, and allows a remote schema change to alter validation behavior without an application deployment.
|
|
149
125
|
|
|
150
|
-
|
|
151
|
-
const schema = {
|
|
152
|
-
type: "object",
|
|
153
|
-
properties: {
|
|
154
|
-
name: { type: "string" },
|
|
155
|
-
age: { type: "number" }
|
|
156
|
-
}
|
|
157
|
-
};
|
|
126
|
+
SchemaShield removes that runtime dependency:
|
|
158
127
|
|
|
159
|
-
|
|
160
|
-
|
|
128
|
+
- No validator-initiated network requests
|
|
129
|
+
- No hidden schema downloads
|
|
130
|
+
- No DNS or remote-host dependency
|
|
131
|
+
- No remote schema substitution during validation
|
|
132
|
+
- No network latency inside the validation path
|
|
161
133
|
|
|
162
|
-
|
|
134
|
+
> Treat external schemas as code dependencies. Review them, version them, and incorporate every required target into the single root schema passed to `compile()`. Deploy that controlled schema with the application.
|
|
163
135
|
|
|
164
|
-
|
|
165
|
-
const data = {
|
|
166
|
-
name: "John Doe",
|
|
167
|
-
age: 30
|
|
168
|
-
};
|
|
136
|
+
### No Runtime Code Generation
|
|
169
137
|
|
|
170
|
-
|
|
138
|
+
SchemaShield does not use `eval()` or `new Function()` to generate executable validators. This keeps validation compatible with strict Content Security Policy environments and lets custom validation functions work directly with JavaScript values, classes, and references.
|
|
171
139
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
} else {
|
|
175
|
-
console.error("Validation error:", validationResult.error);
|
|
176
|
-
}
|
|
177
|
-
```
|
|
140
|
+
```javascript
|
|
141
|
+
import { SchemaShield } from "schema-shield";
|
|
178
142
|
|
|
179
|
-
|
|
143
|
+
const schemaShield = new SchemaShield();
|
|
180
144
|
|
|
181
|
-
-
|
|
182
|
-
- `error`: A `ValidationError` instance if validation failed (when `failFast: false`), `true` if validation failed in fail-fast mode, otherwise `null`.
|
|
183
|
-
- `valid`: true if validation was successful, otherwise false.
|
|
145
|
+
schemaShield.addType("date-class", (data) => data instanceof Date);
|
|
184
146
|
|
|
185
|
-
|
|
147
|
+
class CustomDate extends Date {}
|
|
148
|
+
schemaShield.addType("custom-date-class", (data) => data instanceof CustomDate);
|
|
186
149
|
|
|
187
|
-
|
|
150
|
+
const validateDate = schemaShield.compile({
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
createdAt: { type: "date-class" },
|
|
154
|
+
updatedAt: { type: "custom-date-class" }
|
|
155
|
+
},
|
|
156
|
+
required: ["createdAt", "updatedAt"]
|
|
157
|
+
});
|
|
188
158
|
|
|
189
|
-
|
|
159
|
+
validateDate({
|
|
160
|
+
createdAt: new Date(),
|
|
161
|
+
updatedAt: new CustomDate()
|
|
162
|
+
});
|
|
163
|
+
// { valid: true, data: { createdAt: Date, updatedAt: CustomDate }, error: null }
|
|
164
|
+
```
|
|
190
165
|
|
|
191
|
-
|
|
192
|
-
2. The property is marked as `required` in the schema.
|
|
166
|
+
## Domain-Aware Validation
|
|
193
167
|
|
|
194
|
-
|
|
168
|
+
JSON Schema is commonly used for serialized JSON data. SchemaShield also validates live JavaScript objects, including class instances, Dates, and internal application state.
|
|
195
169
|
|
|
196
|
-
|
|
170
|
+
```javascript
|
|
171
|
+
import { SchemaShield } from "schema-shield";
|
|
197
172
|
|
|
198
|
-
|
|
173
|
+
class Project {
|
|
174
|
+
constructor(name, requiredSkills) {
|
|
175
|
+
this.name = name;
|
|
176
|
+
this.requiredSkills = requiredSkills;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
199
179
|
|
|
200
|
-
|
|
180
|
+
class Employee {
|
|
181
|
+
constructor(name, skills) {
|
|
182
|
+
this.name = name;
|
|
183
|
+
this.skills = skills;
|
|
184
|
+
}
|
|
201
185
|
|
|
202
|
-
|
|
186
|
+
canJoin(project) {
|
|
187
|
+
return project.requiredSkills.every((skill) => this.skills.includes(skill));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const shield = new SchemaShield({ failFast: false });
|
|
203
192
|
|
|
204
|
-
|
|
205
|
-
|
|
193
|
+
shield.addType("project", (data) => data instanceof Project);
|
|
194
|
+
shield.addType("employee", (data) => data instanceof Employee);
|
|
206
195
|
|
|
207
|
-
|
|
196
|
+
shield.addKeyword("qualifiedAssignment", (schema, data, defineError) => {
|
|
197
|
+
if (!schema.qualifiedAssignment) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
208
200
|
|
|
209
|
-
|
|
201
|
+
if (!(data?.project instanceof Project)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
210
204
|
|
|
211
|
-
|
|
205
|
+
if (!(data?.employee instanceof Employee)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
212
208
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
if (!data.employee.canJoin(data.project)) {
|
|
210
|
+
return defineError("Employee does not meet the project's requirements", {
|
|
211
|
+
data
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
219
215
|
|
|
220
|
-
|
|
216
|
+
const validateAssignment = shield.compile({
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
project: { type: "project" },
|
|
220
|
+
employee: { type: "employee" }
|
|
221
|
+
},
|
|
222
|
+
required: ["project", "employee"],
|
|
223
|
+
qualifiedAssignment: true
|
|
224
|
+
});
|
|
221
225
|
|
|
222
|
-
|
|
226
|
+
const project = new Project("Website Redesign", ["HTML", "CSS", "JavaScript"]);
|
|
227
|
+
const employee = new Employee("Ada", ["HTML", "CSS"]);
|
|
223
228
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
| @exodus/schemasafe | ~74% | Interpreter |
|
|
228
|
-
| **SchemaShield** | **~70%** | **Secure Standard** |
|
|
229
|
-
| jsonschema | ~1% | Legacy |
|
|
229
|
+
const result = validateAssignment({ project, employee });
|
|
230
|
+
// { valid: false, data: { project, employee }, error: ValidationError }
|
|
231
|
+
```
|
|
230
232
|
|
|
231
|
-
|
|
233
|
+
This keeps transport validation and domain rules in one validation system without converting live application objects into generated source code.
|
|
232
234
|
|
|
233
|
-
## Edge
|
|
235
|
+
## Edge, Serverless, and Package Support
|
|
234
236
|
|
|
235
|
-
SchemaShield is designed
|
|
237
|
+
SchemaShield is designed for restrictive JavaScript environments and reusable validators.
|
|
236
238
|
|
|
237
|
-
- **Zero
|
|
238
|
-
- **
|
|
239
|
-
- **
|
|
239
|
+
- **Zero runtime dependencies**
|
|
240
|
+
- **Node.js 16.1 or later** for the CJS and ESM package entry points
|
|
241
|
+
- **No runtime code generation**, for environments that prohibit `eval()` and `new Function()`
|
|
242
|
+
- **Synchronous execution**, with no implicit network access
|
|
243
|
+
- **Verified package paths** for CJS, ESM, browser, and TypeScript declarations
|
|
244
|
+
- **Reusable validators** that can be compiled once and shared across requests
|
|
240
245
|
|
|
241
246
|
### Serverless Example
|
|
242
247
|
|
|
248
|
+
Compile validators at module scope and reuse them across invocations:
|
|
249
|
+
|
|
243
250
|
```javascript
|
|
244
|
-
|
|
245
|
-
|
|
251
|
+
import { SchemaShield } from "schema-shield";
|
|
252
|
+
|
|
253
|
+
const validateRequest = new SchemaShield({ failFast: false }).compile({
|
|
246
254
|
type: "object",
|
|
247
255
|
properties: {
|
|
248
256
|
name: { type: "string" },
|
|
@@ -251,403 +259,279 @@ const validateRequest = new SchemaShield().compile({
|
|
|
251
259
|
required: ["name", "email"]
|
|
252
260
|
});
|
|
253
261
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const result = validateRequest(
|
|
257
|
-
|
|
262
|
+
export default async function handler(request) {
|
|
263
|
+
const body = await request.json();
|
|
264
|
+
const result = validateRequest(body);
|
|
265
|
+
|
|
258
266
|
if (!result.valid) {
|
|
259
267
|
return new Response(JSON.stringify({ error: result.error.message }), {
|
|
260
268
|
status: 400,
|
|
261
269
|
headers: { "Content-Type": "application/json" }
|
|
262
270
|
});
|
|
263
271
|
}
|
|
264
|
-
|
|
265
|
-
return new Response(JSON.stringify(result.data), {
|
|
272
|
+
|
|
273
|
+
return new Response(JSON.stringify(result.data), {
|
|
274
|
+
status: 200,
|
|
275
|
+
headers: { "Content-Type": "application/json" }
|
|
276
|
+
});
|
|
266
277
|
}
|
|
267
278
|
```
|
|
268
279
|
|
|
269
|
-
|
|
280
|
+
## Core API
|
|
270
281
|
|
|
271
|
-
|
|
282
|
+
### Import
|
|
272
283
|
|
|
273
284
|
```javascript
|
|
274
|
-
|
|
275
|
-
const validateUser = new SchemaShield().compile(userSchema);
|
|
276
|
-
|
|
277
|
-
// Reuse across requests - no mutable state
|
|
278
|
-
app.get("/user/:id", (req) => {
|
|
279
|
-
const result = validateUser(req.body);
|
|
280
|
-
// ...
|
|
281
|
-
});
|
|
285
|
+
import { SchemaShield } from "schema-shield";
|
|
282
286
|
```
|
|
283
287
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
Unlike JIT compilers that generate code at runtime, SchemaShield uses a flat-loop interpreter:
|
|
287
|
-
|
|
288
|
-
1. **Compilation:** Schema is compiled into a tree of validator functions at compile-time
|
|
289
|
-
2. **Reference Resolution:** $ref links are resolved once during compilation
|
|
290
|
-
3. **Validation:** Data flows through the validator tree in a flat loop (no recursion)
|
|
291
|
-
|
|
292
|
-
### Performance Characteristics
|
|
293
|
-
|
|
294
|
-
| Schema Type | Performance | Notes |
|
|
295
|
-
|-------------|-------------|-------|
|
|
296
|
-
| Simple (type, properties) | Very fast | ~same as JIT |
|
|
297
|
-
| allOf/anyOf/oneOf | ~70% JIT | Sequential branch evaluation |
|
|
298
|
-
| Deep nesting | Stack-safe | Constant memory, no recursion |
|
|
299
|
-
| Many $refs | Fast | Resolved at compile-time |
|
|
300
|
-
|
|
301
|
-
### Performance Optimization Tips
|
|
288
|
+
CommonJS is also supported:
|
|
302
289
|
|
|
303
290
|
```javascript
|
|
304
|
-
|
|
305
|
-
const fastSchema = {
|
|
306
|
-
type: "object",
|
|
307
|
-
properties: {
|
|
308
|
-
name: { type: "string" },
|
|
309
|
-
email: { type: "string", format: "email" }
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// Slower: Deeply nested allOf/anyOf
|
|
314
|
-
const slowSchema = {
|
|
315
|
-
allOf: [
|
|
316
|
-
{ allOf: [{ allOf: [...] }]}
|
|
317
|
-
]
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
// Better: Flatten your schema
|
|
321
|
-
const betterSchema = {
|
|
322
|
-
type: "object",
|
|
323
|
-
properties: { /* ... */ }
|
|
324
|
-
};
|
|
291
|
+
const { SchemaShield } = require("schema-shield");
|
|
325
292
|
```
|
|
326
293
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
- Supports draft-06 and draft-07 of the [JSON Schema](https://json-schema.org/) specification.
|
|
330
|
-
- Full support for internal references ($ref) and anchors ($id).
|
|
331
|
-
- No Code Generation for Enhanced Safety and Validation Flexibility.
|
|
332
|
-
- Custom type, keyword, and format validators.
|
|
333
|
-
- Runtime object validation: first-class support for business logic checks (type checking and schema validation).
|
|
334
|
-
- Immutable mode for data protection.
|
|
335
|
-
- Lightweight and fast.
|
|
336
|
-
- Easy to use and extend.
|
|
337
|
-
- No dependencies.
|
|
338
|
-
- TypeScript support.
|
|
339
|
-
|
|
340
|
-
## Security Philosophy: Hermetic Validation
|
|
341
|
-
|
|
342
|
-
SchemaShield adopts a **Zero Trust** and **Hermetic Architecture** approach. Unlike validators that allow runtime network access, SchemaShield is strictly synchronous and offline by design.
|
|
343
|
-
|
|
344
|
-
### Why No Remote References?
|
|
345
|
-
|
|
346
|
-
Allowing a validator to fetch schemas from remote URLs (`$ref: "https://..."`) at runtime introduces critical security vectors and stability issues:
|
|
347
|
-
|
|
348
|
-
1. **SSRF (Server-Side Request Forgery):** Prevents attackers from manipulating schemas to force internal network scanning or access metadata services.
|
|
349
|
-
2. **Supply Chain Attacks:** Eliminates the risk of a remote schema being silently compromised, which could alter validation logic without code deployment.
|
|
350
|
-
3. **Deterministic Reliability:** Validation never fails due to network latency, DNS issues, or third-party server downtime.
|
|
351
|
-
|
|
352
|
-
**Recommendation:** Treat schemas as **code dependencies**, not dynamic assets. Download and bundle remote schemas locally during your build process to ensure immutable, versioned, and audit-ready validation.
|
|
353
|
-
|
|
354
|
-
## No Code Generation
|
|
355
|
-
|
|
356
|
-
Unlike some other validation libraries that rely on code generation to achieve fast performance, SchemaShield does not use code generation.
|
|
357
|
-
|
|
358
|
-
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.
|
|
359
|
-
|
|
360
|
-
For example, you can easily use `instanceof` to check if the provided data is an instance of a particular class or a subclass:
|
|
294
|
+
### Create an Instance
|
|
361
295
|
|
|
362
296
|
```javascript
|
|
363
|
-
schemaShield
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
297
|
+
const schemaShield = new SchemaShield({
|
|
298
|
+
immutable: false,
|
|
299
|
+
failFast: true,
|
|
300
|
+
maxDepth: 128,
|
|
301
|
+
useDefaults: false
|
|
302
|
+
});
|
|
367
303
|
```
|
|
368
304
|
|
|
369
|
-
|
|
305
|
+
| Option | Default | Behavior |
|
|
306
|
+
| :------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
307
|
+
| `immutable` | `false` | Set to `true` to preserve the original input when defaults or custom keywords could modify data. |
|
|
308
|
+
| `failFast` | `true` | Returns `error: true` on failure. With `false`, built-ins and custom failures created with `defineError()` return a detailed `ValidationError`. A custom keyword that returns `true` directly still returns `error: true`. |
|
|
309
|
+
| `maxDepth` | `128` | Sets the maximum recursive validation depth. Accepts an integer from `1` to `256`; validation paths that exceed it fail with a controlled error. |
|
|
310
|
+
| `useDefaults` | `false` | Leaves `default` as a JSON Schema annotation. Use `true` to replace absent or `undefined` properties, or `"empty"` to also replace `null` and empty strings. |
|
|
370
311
|
|
|
371
|
-
|
|
312
|
+
Schema compilation applies its own depth limit before a validator is created. This compile-time protection is separate from the runtime `maxDepth` option.
|
|
372
313
|
|
|
373
|
-
|
|
314
|
+
### Compile and Validate
|
|
374
315
|
|
|
375
316
|
```javascript
|
|
376
|
-
|
|
377
|
-
const validator = new SchemaShield().compile({
|
|
317
|
+
const schema = {
|
|
378
318
|
type: "object",
|
|
379
319
|
properties: {
|
|
380
|
-
name: { type: "string" }
|
|
381
|
-
|
|
382
|
-
}
|
|
320
|
+
name: { type: "string" },
|
|
321
|
+
age: { type: "number" }
|
|
322
|
+
},
|
|
323
|
+
required: ["name"]
|
|
324
|
+
};
|
|
383
325
|
|
|
384
|
-
|
|
385
|
-
const result = validator({ name: "
|
|
326
|
+
const validator = schemaShield.compile(schema);
|
|
327
|
+
const result = validator({ name: "Ada", age: 37 });
|
|
386
328
|
```
|
|
387
329
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
SchemaShield prevents prototype pollution by design:
|
|
330
|
+
Every result contains:
|
|
391
331
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
properties: {
|
|
396
|
-
name: { type: "string" }
|
|
397
|
-
},
|
|
398
|
-
additionalProperties: false
|
|
399
|
-
};
|
|
332
|
+
- `valid`: `true` when validation succeeds, otherwise `false`.
|
|
333
|
+
- `data`: The validated data, including any applied defaults.
|
|
334
|
+
- `error`: `null` on success, `true` on a fail-fast failure or when a custom keyword returns `true` directly, or a `ValidationError` for built-in failures and custom failures created with `defineError()` when `failFast: false`.
|
|
400
335
|
|
|
401
|
-
|
|
336
|
+
### Defaults
|
|
402
337
|
|
|
403
|
-
|
|
404
|
-
validator({ "__proto__": { "evil": "value" } });
|
|
405
|
-
// { valid: false, error: ValidationError }
|
|
338
|
+
SchemaShield keeps data shaping explicit. By default, `default` remains a standard JSON Schema annotation and your input stays unchanged.
|
|
406
339
|
|
|
407
|
-
|
|
408
|
-
validator({ name: "John", age: 30 });
|
|
409
|
-
// { valid: false, error: ValidationError (additionalProperties: false) }
|
|
410
|
-
```
|
|
340
|
+
Enable `useDefaults` when you want validated data to apply defaults and leave the validator ready to use:
|
|
411
341
|
|
|
412
|
-
|
|
342
|
+
| `useDefaults` | Behavior |
|
|
343
|
+
| :----------------- | :------------------------------------------------------------------------------------ |
|
|
344
|
+
| Omitted or `false` | Keeps defaults as annotations and does not modify data. |
|
|
345
|
+
| `true` | Completes missing required and optional properties. |
|
|
346
|
+
| `"empty"` | Also replaces `null` and `""`, while preserving valid values such as `0` and `false`. |
|
|
413
347
|
|
|
414
|
-
|
|
348
|
+
Combine `useDefaults` with `immutable: true` to receive completed data without modifying the original object:
|
|
415
349
|
|
|
416
350
|
```javascript
|
|
417
|
-
const
|
|
418
|
-
|
|
351
|
+
const validateUser = new SchemaShield({
|
|
352
|
+
useDefaults: true,
|
|
353
|
+
immutable: true
|
|
354
|
+
}).compile({
|
|
419
355
|
type: "object",
|
|
420
356
|
properties: {
|
|
421
|
-
|
|
422
|
-
|
|
357
|
+
role: { type: "string", default: "member" },
|
|
358
|
+
theme: { type: "string", default: "system" }
|
|
359
|
+
},
|
|
360
|
+
required: ["role"]
|
|
423
361
|
});
|
|
424
362
|
|
|
425
|
-
const input = {
|
|
426
|
-
const result =
|
|
363
|
+
const input = {};
|
|
364
|
+
const result = validateUser(input);
|
|
427
365
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
// { name: "John" }
|
|
431
|
-
```
|
|
366
|
+
input;
|
|
367
|
+
// {}
|
|
432
368
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
369
|
+
result.data;
|
|
370
|
+
// { role: "member", theme: "system" }
|
|
371
|
+
```
|
|
436
372
|
|
|
437
|
-
|
|
438
|
-
- If the instance was created with `{ failFast: false }`, the `error` property will contain a `ValidationError` instance with rich debugging information.
|
|
373
|
+
## Errors and Debugging
|
|
439
374
|
|
|
440
|
-
|
|
375
|
+
SchemaShield provides two error modes:
|
|
441
376
|
|
|
442
|
-
|
|
377
|
+
- `failFast: true` returns `error: true` with minimal allocation.
|
|
378
|
+
- `failFast: false` returns a `ValidationError` with rich debugging information for built-in failures and custom keyword failures created with `defineError()`. A custom keyword that returns `true` directly still produces `error: true`. Use `defineError()` for detailed errors.
|
|
443
379
|
|
|
444
380
|
```javascript
|
|
445
381
|
import { SchemaShield } from "schema-shield";
|
|
446
382
|
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
const schema = {
|
|
383
|
+
const validatePerson = new SchemaShield({ failFast: false }).compile({
|
|
450
384
|
type: "object",
|
|
451
385
|
properties: {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
const validator = schemaShield.compile(schema);
|
|
461
|
-
|
|
462
|
-
const invalidData = {
|
|
463
|
-
name: "John Doe",
|
|
464
|
-
age: 15
|
|
465
|
-
};
|
|
386
|
+
age: { type: "number", minimum: 18 }
|
|
387
|
+
},
|
|
388
|
+
required: ["age"]
|
|
389
|
+
});
|
|
466
390
|
|
|
467
|
-
const
|
|
391
|
+
const result = validatePerson({ age: 15 });
|
|
468
392
|
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
} else {
|
|
472
|
-
console.error("Validation error:", validationResult.error.message); // "Property is invalid"
|
|
393
|
+
if (!result.valid) {
|
|
394
|
+
const paths = result.error.getPath();
|
|
473
395
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
console.error(
|
|
477
|
-
console.error("Instance path:", errorPaths.instancePath); // "#/age"
|
|
396
|
+
console.error(result.error.message);
|
|
397
|
+
console.error(paths.schemaPath);
|
|
398
|
+
console.error(paths.instancePath);
|
|
478
399
|
}
|
|
479
400
|
```
|
|
480
401
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
## Adding Custom Types
|
|
484
|
-
|
|
485
|
-
SchemaShield allows you to add custom types for validation using the `addType` method.
|
|
486
|
-
|
|
487
|
-
### Method Signature
|
|
402
|
+
### ValidationError Properties
|
|
488
403
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
404
|
+
- `message`: Description of the validation error.
|
|
405
|
+
- `item`: Final item in the failing path, when available.
|
|
406
|
+
- `keyword`: Keyword that triggered the error.
|
|
407
|
+
- `cause`: Nested `ValidationError` that caused the current error.
|
|
408
|
+
- `code`: Stable error code, when the error provides one.
|
|
409
|
+
- `schemaPath`: JSON Pointer to the failing schema location.
|
|
410
|
+
- `instancePath`: JSON Pointer to the failing data location.
|
|
411
|
+
- `data`: Data that caused the error, when available.
|
|
412
|
+
- `schema`: Schema value that caused the error, when available.
|
|
493
413
|
|
|
494
|
-
|
|
495
|
-
```
|
|
414
|
+
`schemaPath` and `instancePath` become available after calling `getCause()`, `getTree()`, or `getPath()`.
|
|
496
415
|
|
|
497
|
-
|
|
498
|
-
- `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.
|
|
499
|
-
- `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.
|
|
416
|
+
### Error Methods
|
|
500
417
|
|
|
501
|
-
|
|
418
|
+
#### `getPath()`
|
|
502
419
|
|
|
503
|
-
|
|
420
|
+
Returns the schema and instance paths for the root validation failure.
|
|
504
421
|
|
|
505
422
|
```javascript
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const schemaShield = new SchemaShield({ failFast: false });
|
|
423
|
+
const { schemaPath, instancePath } = result.error.getPath();
|
|
424
|
+
```
|
|
509
425
|
|
|
510
|
-
|
|
511
|
-
const ageValidator = (data) => {
|
|
512
|
-
return typeof data === "number" && data >= 18 && data <= 120;
|
|
513
|
-
};
|
|
426
|
+
#### `getCause()`
|
|
514
427
|
|
|
515
|
-
|
|
516
|
-
schemaShield.addType("age", ageValidator);
|
|
428
|
+
Returns the root `ValidationError` that caused the validation chain.
|
|
517
429
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
age: { type: "age" }
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
const validator = schemaShield.compile(schema);
|
|
430
|
+
```javascript
|
|
431
|
+
const cause = result.error.getCause();
|
|
432
|
+
console.error(cause.message, cause.keyword);
|
|
433
|
+
```
|
|
527
434
|
|
|
528
|
-
|
|
529
|
-
name: "John Doe",
|
|
530
|
-
age: 25
|
|
531
|
-
};
|
|
435
|
+
#### `getTree()`
|
|
532
436
|
|
|
533
|
-
|
|
437
|
+
Returns the complete nested error chain as an error tree.
|
|
534
438
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
439
|
+
```typescript
|
|
440
|
+
interface ErrorTree {
|
|
441
|
+
message: string;
|
|
442
|
+
keyword: string;
|
|
443
|
+
item?: string | number;
|
|
444
|
+
schemaPath: string;
|
|
445
|
+
instancePath: string;
|
|
446
|
+
data?: any;
|
|
447
|
+
cause?: ErrorTree;
|
|
539
448
|
}
|
|
540
449
|
```
|
|
541
450
|
|
|
542
|
-
##
|
|
451
|
+
## Extensibility
|
|
543
452
|
|
|
544
|
-
SchemaShield
|
|
453
|
+
SchemaShield exposes custom types, formats, and keywords through the same instance used to compile validators.
|
|
545
454
|
|
|
546
|
-
|
|
455
|
+
The examples in this section use the following instance:
|
|
547
456
|
|
|
548
457
|
```javascript
|
|
549
|
-
|
|
550
|
-
(data: any): boolean;
|
|
551
|
-
}
|
|
458
|
+
import { SchemaShield } from "schema-shield";
|
|
552
459
|
|
|
553
|
-
|
|
460
|
+
const schemaShield = new SchemaShield();
|
|
554
461
|
```
|
|
555
462
|
|
|
556
|
-
|
|
557
|
-
- `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.
|
|
558
|
-
- `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.
|
|
463
|
+
### Custom Types
|
|
559
464
|
|
|
560
|
-
|
|
465
|
+
```typescript
|
|
466
|
+
interface TypeFunction {
|
|
467
|
+
(data: any): boolean;
|
|
468
|
+
}
|
|
561
469
|
|
|
562
|
-
|
|
470
|
+
interface SchemaShieldTypeAPI {
|
|
471
|
+
addType(name: string, validator: TypeFunction, overwrite?: boolean): void;
|
|
472
|
+
}
|
|
473
|
+
```
|
|
563
474
|
|
|
564
475
|
```javascript
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
476
|
+
schemaShield.addType(
|
|
477
|
+
"adult-age",
|
|
478
|
+
(data) => typeof data === "number" && data >= 18
|
|
479
|
+
);
|
|
568
480
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const ssnPattern = /^(?!000|.+0{4})(?:\d{9}|\d{3}-\d{2}-\d{4})$/;
|
|
572
|
-
return typeof data === "string" && ssnPattern.test(data);
|
|
573
|
-
};
|
|
481
|
+
const validator = schemaShield.compile({ type: "adult-age" });
|
|
482
|
+
```
|
|
574
483
|
|
|
575
|
-
|
|
576
|
-
schemaShield.addFormat("ssn", ssnValidator);
|
|
484
|
+
Set `overwrite` to `true` to replace an existing type with the same name. The default is `false`.
|
|
577
485
|
|
|
578
|
-
|
|
579
|
-
type: "object",
|
|
580
|
-
properties: {
|
|
581
|
-
name: { type: "string" },
|
|
582
|
-
ssn: { type: "string", format: "ssn" }
|
|
583
|
-
}
|
|
584
|
-
};
|
|
486
|
+
### Custom Formats
|
|
585
487
|
|
|
586
|
-
|
|
488
|
+
```typescript
|
|
489
|
+
interface FormatFunction {
|
|
490
|
+
(data: any): boolean;
|
|
491
|
+
}
|
|
587
492
|
|
|
588
|
-
|
|
589
|
-
name:
|
|
590
|
-
|
|
591
|
-
|
|
493
|
+
interface SchemaShieldFormatAPI {
|
|
494
|
+
addFormat(name: string, validator: FormatFunction, overwrite?: boolean): void;
|
|
495
|
+
}
|
|
496
|
+
```
|
|
592
497
|
|
|
593
|
-
|
|
498
|
+
```javascript
|
|
499
|
+
schemaShield.addFormat(
|
|
500
|
+
"username",
|
|
501
|
+
(data) => typeof data === "string" && /^[a-z0-9._-]{3,}$/i.test(data)
|
|
502
|
+
);
|
|
594
503
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
504
|
+
const validator = schemaShield.compile({
|
|
505
|
+
type: "string",
|
|
506
|
+
format: "username"
|
|
507
|
+
});
|
|
600
508
|
```
|
|
601
509
|
|
|
602
|
-
|
|
510
|
+
Set `overwrite` to `true` to replace an existing format with the same name. The default is `false`.
|
|
603
511
|
|
|
604
|
-
|
|
512
|
+
### Custom Keywords
|
|
605
513
|
|
|
606
|
-
|
|
514
|
+
```typescript
|
|
515
|
+
import type {
|
|
516
|
+
CompiledSchema,
|
|
517
|
+
SchemaShield,
|
|
518
|
+
ValidationError
|
|
519
|
+
} from "schema-shield";
|
|
607
520
|
|
|
608
|
-
```javascript
|
|
609
521
|
type Result = void | ValidationError | true;
|
|
610
522
|
|
|
611
523
|
interface DefineErrorOptions {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
524
|
+
code?: string;
|
|
525
|
+
item?: any;
|
|
526
|
+
cause?: ValidationError | true;
|
|
527
|
+
data?: any;
|
|
615
528
|
}
|
|
616
529
|
|
|
617
530
|
interface DefineErrorFunction {
|
|
618
|
-
(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
(data: any): Result;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
interface CompiledSchema {
|
|
626
|
-
$validate?: ValidateFunction;
|
|
627
|
-
[key: string]: any;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
interface FormatFunction {
|
|
631
|
-
(data: any): boolean;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
interface TypeFunction {
|
|
635
|
-
(data: any): boolean;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
declare class SchemaShield {
|
|
639
|
-
constructor(options?: {
|
|
640
|
-
immutable?: boolean;
|
|
641
|
-
failFast?: boolean;
|
|
642
|
-
});
|
|
643
|
-
compile(schema: any): Validator;
|
|
644
|
-
addType(name: string, validator: TypeFunction, overwrite?: boolean): void;
|
|
645
|
-
addFormat(name: string, validator: FormatFunction, overwrite?: boolean): void;
|
|
646
|
-
addKeyword(name: string, validator: KeywordFunction, overwrite?: boolean): void;
|
|
647
|
-
getType(type: string): TypeFunction | false;
|
|
648
|
-
getFormat(format: string): FormatFunction | false;
|
|
649
|
-
getKeyword(keyword: string): KeywordFunction | false;
|
|
650
|
-
isSchemaLike(subSchema: any): boolean;
|
|
531
|
+
(
|
|
532
|
+
message: string,
|
|
533
|
+
options?: DefineErrorOptions
|
|
534
|
+
): ValidationError | void | true;
|
|
651
535
|
}
|
|
652
536
|
|
|
653
537
|
interface KeywordFunction {
|
|
@@ -659,46 +543,21 @@ interface KeywordFunction {
|
|
|
659
543
|
): Result;
|
|
660
544
|
}
|
|
661
545
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
546
|
+
interface SchemaShieldKeywordAPI {
|
|
547
|
+
addKeyword(
|
|
548
|
+
name: string,
|
|
549
|
+
validator: KeywordFunction,
|
|
550
|
+
overwrite?: boolean
|
|
551
|
+
): void;
|
|
552
|
+
}
|
|
665
553
|
```
|
|
666
554
|
|
|
667
|
-
|
|
668
|
-
- `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 when `failFast` is `false`, or `true` when `failFast` is `true`.
|
|
669
|
-
- `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.
|
|
670
|
-
|
|
671
|
-
#### About the `defineError` Function
|
|
672
|
-
|
|
673
|
-
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).
|
|
674
|
-
|
|
675
|
-
- `message`: A string that describes the validation error.
|
|
676
|
-
- `options`: An optional object with properties that provide more context for the error:
|
|
677
|
-
- `item`?: An optional value representing the final item in the path where the validation error occurred. (e.g. index of an array item)
|
|
678
|
-
- `cause`?: An optional `ValidationError` (or `true` in fail-fast mode) that represents the cause of the current error.
|
|
679
|
-
- `data`?: An optional value representing the data that caused the validation error.
|
|
680
|
-
|
|
681
|
-
When the SchemaShield instance is created with `failFast: true`, `defineError` returns `true` instead of a `ValidationError`, and keyword implementations should simply `return` whatever `defineError` gives them.
|
|
682
|
-
|
|
683
|
-
#### About the `instance` Argument
|
|
684
|
-
|
|
685
|
-
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.
|
|
686
|
-
|
|
687
|
-
### Example: Adding a Custom Keyword
|
|
688
|
-
|
|
689
|
-
In this example, we'll add a custom keyword called divisibleBy that validates if a given number is divisible by a specified divisor.
|
|
555
|
+
Use `defineError()` so SchemaShield can attach the current keyword, schema, data, and error-chain context:
|
|
690
556
|
|
|
691
557
|
```javascript
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const schemaShield = new SchemaShield({ failFast: false });
|
|
695
|
-
|
|
696
|
-
// Custom keyword 'divisibleBy' validator function
|
|
697
|
-
const divisibleByValidator = (schema, data, defineError, instance) => {
|
|
558
|
+
schemaShield.addKeyword("divisibleBy", (schema, data, defineError) => {
|
|
698
559
|
if (typeof data !== "number") {
|
|
699
|
-
return defineError("Value must be a number", {
|
|
700
|
-
data
|
|
701
|
-
});
|
|
560
|
+
return defineError("Value must be a number", { data });
|
|
702
561
|
}
|
|
703
562
|
|
|
704
563
|
if (data % schema.divisibleBy !== 0) {
|
|
@@ -706,603 +565,94 @@ const divisibleByValidator = (schema, data, defineError, instance) => {
|
|
|
706
565
|
data
|
|
707
566
|
});
|
|
708
567
|
}
|
|
709
|
-
};
|
|
710
|
-
|
|
711
|
-
// Adding the custom keyword 'divisibleBy'
|
|
712
|
-
schemaShield.addKeyword("divisibleBy", divisibleByValidator);
|
|
713
|
-
|
|
714
|
-
const schema = {
|
|
715
|
-
type: "object",
|
|
716
|
-
properties: {
|
|
717
|
-
value: { type: "number", divisibleBy: 5 }
|
|
718
|
-
}
|
|
719
|
-
};
|
|
720
|
-
|
|
721
|
-
const validator = schemaShield.compile(schema);
|
|
722
|
-
|
|
723
|
-
const validData = {
|
|
724
|
-
value: 15
|
|
725
|
-
};
|
|
726
|
-
|
|
727
|
-
const validationResult = validator(validData);
|
|
728
|
-
|
|
729
|
-
if (validationResult.valid) {
|
|
730
|
-
console.log("Data is valid:", validationResult.data);
|
|
731
|
-
} else {
|
|
732
|
-
console.error("Validation error:", validationResult.error.getCause().message);
|
|
733
|
-
}
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
### Complex example: Adding a Custom Keyword that uses the instance
|
|
737
|
-
|
|
738
|
-
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.
|
|
739
|
-
|
|
740
|
-
```javascript
|
|
741
|
-
import { SchemaShield, ValidationError } from "schema-shield";
|
|
742
|
-
|
|
743
|
-
const schemaShield = new SchemaShield({ failFast: false });
|
|
744
|
-
|
|
745
|
-
// Custom type validator: nonEmptyString
|
|
746
|
-
const nonEmptyStringValidator = (data) =>
|
|
747
|
-
typeof data === "string" && data.length > 0;
|
|
748
|
-
schemaShield.addType("nonEmptyString", nonEmptyStringValidator);
|
|
749
|
-
|
|
750
|
-
// Custom keyword validator: hasPrefix
|
|
751
|
-
const hasPrefixValidator = (schema, data, defineError, instance) => {
|
|
752
|
-
const { prefix } = schema.hasPrefix;
|
|
753
|
-
if (typeof data === "string" && !data.startsWith(prefix)) {
|
|
754
|
-
return defineError(`String must have the prefix "${prefix}"`, {
|
|
755
|
-
data
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
};
|
|
759
|
-
schemaShield.addKeyword("hasPrefix", hasPrefixValidator);
|
|
760
|
-
|
|
761
|
-
// Custom format validator: username
|
|
762
|
-
const usernameValidator = (data) => /^[a-zA-Z0-9._-]{3,}$/i.test(data);
|
|
763
|
-
schemaShield.addFormat("username", usernameValidator);
|
|
764
|
-
|
|
765
|
-
// Custom keyword 'prefixedUsername' validator function
|
|
766
|
-
const prefixedUsername = (schema, data, defineError, instance) => {
|
|
767
|
-
const { validType, prefixValidator, validFormat } = schema.prefixedUsername;
|
|
768
|
-
|
|
769
|
-
// Get the validators for the specified types and formats from the instance
|
|
770
|
-
// (if they exist)
|
|
771
|
-
const typeValidator = instance.getType(validType);
|
|
772
|
-
const prefixKeyword = instance.getKeyword(prefixValidator);
|
|
773
|
-
const formatValidator = instance.getFormat(validFormat);
|
|
774
|
-
|
|
775
|
-
for (let i = 0; i < data.length; i++) {
|
|
776
|
-
const item = data[i];
|
|
777
|
-
|
|
778
|
-
// Validate that the data is of the correct type if specified
|
|
779
|
-
if (validType && typeValidator) {
|
|
780
|
-
if (!typeValidator(item)) {
|
|
781
|
-
return defineError(`Invalid type: ${validType}`, {
|
|
782
|
-
item: i,
|
|
783
|
-
data: data[i]
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// Validate that the data has the correct format if specified
|
|
789
|
-
if (validFormat && formatValidator) {
|
|
790
|
-
if (!formatValidator(item)) {
|
|
791
|
-
return defineError(`Invalid format: ${validFormat}`, {
|
|
792
|
-
item: i,
|
|
793
|
-
data: data[i]
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Validate that the data has the correct prefix if specified
|
|
799
|
-
if (prefixKeyword) {
|
|
800
|
-
const error = prefixKeyword(schema, item, defineError, instance);
|
|
801
|
-
if (error) {
|
|
802
|
-
return defineError(`Invalid prefix: ${prefixValidator}`, {
|
|
803
|
-
cause: error,
|
|
804
|
-
item: i,
|
|
805
|
-
data: data[i]
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
};
|
|
811
|
-
|
|
812
|
-
schemaShield.addKeyword("prefixedUsername", prefixedUsername);
|
|
813
|
-
|
|
814
|
-
const schema = {
|
|
815
|
-
type: "array",
|
|
816
|
-
prefixedUsername: {
|
|
817
|
-
validType: "nonEmptyString",
|
|
818
|
-
prefixValidator: "hasPrefix",
|
|
819
|
-
validFormat: "username"
|
|
820
|
-
},
|
|
821
|
-
items: {
|
|
822
|
-
type: "string"
|
|
823
|
-
}
|
|
824
|
-
};
|
|
825
|
-
|
|
826
|
-
const validator = schemaShield.compile(schema);
|
|
827
|
-
|
|
828
|
-
const validData = ["user.john", "user.jane"];
|
|
829
|
-
|
|
830
|
-
const validationResult = validator(validData);
|
|
831
|
-
|
|
832
|
-
if (validationResult.valid) {
|
|
833
|
-
console.log("Data is valid:", validationResult.data);
|
|
834
|
-
} else {
|
|
835
|
-
console.error("Validation error:", validationResult.error.getCause().message);
|
|
836
|
-
}
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
## Supported Formats
|
|
840
|
-
|
|
841
|
-
SchemaShield includes built-in validators for the following formats:
|
|
842
|
-
|
|
843
|
-
- **Date & Time:** `date`, `time`, `date-time`, `duration`.
|
|
844
|
-
- **Email:** `email`, `idn-email`.
|
|
845
|
-
- **Hostnames:** `hostname`, `idn-hostname`.
|
|
846
|
-
- **IP Addresses:** `ipv4`, `ipv6`.
|
|
847
|
-
- **Resource Identifiers:** `uuid`, `uri`, `uri-reference`, `uri-template`, `iri`, `iri-reference`.
|
|
848
|
-
- **JSON Pointers:** `json-pointer`, `relative-json-pointer`.
|
|
849
|
-
- **Regex:** `regex`.
|
|
850
|
-
|
|
851
|
-
You can override any of these or add new ones using `schemaShield.addFormat`.
|
|
852
|
-
|
|
853
|
-
## Validating Runtime Objects
|
|
854
|
-
|
|
855
|
-
JSON Schema is traditionally for serialized JSON text. SchemaShield extends this concept to **JavaScript Objects**. It allows validation of class instances, Dates, and internal application state directly.
|
|
856
|
-
|
|
857
|
-
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:
|
|
858
|
-
|
|
859
|
-
```javascript
|
|
860
|
-
import { SchemaShield, ValidationError } from "schema-shield";
|
|
861
|
-
|
|
862
|
-
const schemaShield = new SchemaShield({ failFast: false });
|
|
863
|
-
|
|
864
|
-
// Custom classes
|
|
865
|
-
class Project {
|
|
866
|
-
constructor(name: string, requiredSkills: string[]) {
|
|
867
|
-
this.name = name;
|
|
868
|
-
this.requiredSkills = requiredSkills;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
class Employee {
|
|
873
|
-
constructor(name: string, skills: string[]) {
|
|
874
|
-
this.name = name;
|
|
875
|
-
this.skills = skills;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
hasSkillsForProject(project: Project) {
|
|
879
|
-
return project.requiredSkills.every((skill) => this.skills.includes(skill));
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// Add custom types to the instance
|
|
884
|
-
schemaShield.addType("project", (data) => data instanceof Project);
|
|
885
|
-
schemaShield.addType("employee", (data) => data instanceof Employee);
|
|
886
|
-
|
|
887
|
-
schemaShield.addKeyword(
|
|
888
|
-
"requiresQualifiedEmployee",
|
|
889
|
-
(schema, data, defineError, instance) => {
|
|
890
|
-
const { assignment, project, employee } = data;
|
|
891
|
-
|
|
892
|
-
const stringTypeValidator = instance.getType("string");
|
|
893
|
-
const projectTypeValidator = instance.getType("project");
|
|
894
|
-
const employeeTypeValidator = instance.getType("employee");
|
|
895
|
-
|
|
896
|
-
if (!stringTypeValidator(assignment)) {
|
|
897
|
-
return defineError("Assignment must be a string", {
|
|
898
|
-
item: "assignment",
|
|
899
|
-
data: assignment
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
if (!projectTypeValidator(project)) {
|
|
904
|
-
return defineError("Project must be a Project instance", {
|
|
905
|
-
item: "project",
|
|
906
|
-
data: project
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
if (!employeeTypeValidator(employee)) {
|
|
911
|
-
return defineError("Employee must be an Employee instance", {
|
|
912
|
-
item: "employee",
|
|
913
|
-
data: employee
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
if (schema.requiresQualifiedEmployee) {
|
|
918
|
-
if (!employee.hasSkillsForProject(project)) {
|
|
919
|
-
return defineError(
|
|
920
|
-
"Employee does not meet the project's requirements",
|
|
921
|
-
{
|
|
922
|
-
data: {
|
|
923
|
-
assignment,
|
|
924
|
-
project,
|
|
925
|
-
employee
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
);
|
|
933
|
-
|
|
934
|
-
// Create and compile the schema
|
|
935
|
-
const schema = {
|
|
936
|
-
type: "object",
|
|
937
|
-
properties: {
|
|
938
|
-
assignment: {}, // Empty schema because we will validate it with the custom keyword
|
|
939
|
-
project: {}, // Empty schema because we will validate it with the custom keyword
|
|
940
|
-
employee: {} // Empty schema because we will validate it with the custom keyword
|
|
941
|
-
},
|
|
942
|
-
required: ["assignment", "project", "employee"],
|
|
943
|
-
requiresQualifiedEmployee: true
|
|
944
|
-
};
|
|
945
|
-
|
|
946
|
-
const validator = schemaShield.compile(schema);
|
|
947
|
-
|
|
948
|
-
// Create some data to validate
|
|
949
|
-
const employee1 = new Employee("Employee 1", ["A", "B", "C"]);
|
|
950
|
-
|
|
951
|
-
const project1 = new Project("Project 1", ["A", "B"]);
|
|
952
|
-
|
|
953
|
-
const dataToValidate = {
|
|
954
|
-
assignment: "Assignment 1 for Project 1",
|
|
955
|
-
project: project1,
|
|
956
|
-
employee: employee1
|
|
957
|
-
};
|
|
958
|
-
|
|
959
|
-
// Validate the data
|
|
960
|
-
const validationResult = validator(dataToValidate);
|
|
961
|
-
|
|
962
|
-
if (validationResult.valid) {
|
|
963
|
-
console.log("Assignment is valid:", validationResult.data);
|
|
964
|
-
} else {
|
|
965
|
-
console.error("Validation error:", validationResult.error.message);
|
|
966
|
-
}
|
|
967
|
-
```
|
|
968
|
-
|
|
969
|
-
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.
|
|
970
|
-
|
|
971
|
-
## More on Error Handling
|
|
972
|
-
|
|
973
|
-
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 when `failFast: false` is used; otherwise `error` is `true`.
|
|
974
|
-
|
|
975
|
-
This error instance uses the [`Error.cause`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) property. This allows you to analyze the whole error chain or retrieve the root cause of the error using the `getCause()`, `getTree()`, and `getPath()` methods.
|
|
976
|
-
|
|
977
|
-
### ValidationError Properties
|
|
978
|
-
|
|
979
|
-
- `message`: A string containing a description of the error.
|
|
980
|
-
- `item`: The final item in the path that caused the error (either a string or a number) (optional).
|
|
981
|
-
- `keyword`: The keyword that triggered the error.
|
|
982
|
-
- `cause`: A nested ValidationError or a normal Error that caused the current error.
|
|
983
|
-
- `schemaPath`: The JSON Pointer path to the error location in the schema.
|
|
984
|
-
- `instancePath`: The JSON Pointer path to the error location in the data.
|
|
985
|
-
- `data`: The data that caused the error (optional).
|
|
986
|
-
- `schema`: The schema that caused the error (optional).
|
|
987
|
-
|
|
988
|
-
_Note:_ The `schemaPath` and `instancePath` will be only available after using the `getCause()` `getTree()` or `getPath()` methods.
|
|
989
|
-
|
|
990
|
-
### Get the path to the error location
|
|
991
|
-
|
|
992
|
-
You can use the `getPath` method to get the JSON Pointer path to the error location in the schema and in the data. This method returns an object containing the `schemaPath` and `instancePath`.
|
|
993
|
-
|
|
994
|
-
**Example:**
|
|
995
|
-
|
|
996
|
-
```javascript
|
|
997
|
-
import { SchemaShield } from "schema-shield";
|
|
998
|
-
|
|
999
|
-
const schemaShield = new SchemaShield({ failFast: false });
|
|
1000
|
-
|
|
1001
|
-
const schema = {
|
|
1002
|
-
type: "object",
|
|
1003
|
-
properties: {
|
|
1004
|
-
description: { type: "string" },
|
|
1005
|
-
shouldLoadDb: { type: "boolean" },
|
|
1006
|
-
enableNetConnectFor: { type: "array", items: { type: "string" } },
|
|
1007
|
-
params: {
|
|
1008
|
-
type: "object",
|
|
1009
|
-
additionalProperties: {
|
|
1010
|
-
type: "object",
|
|
1011
|
-
properties: {
|
|
1012
|
-
description: { type: "string" },
|
|
1013
|
-
default: { type: "string" }
|
|
1014
|
-
},
|
|
1015
|
-
required: ["description"]
|
|
1016
|
-
}
|
|
1017
|
-
},
|
|
1018
|
-
run: { type: "string" }
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
1021
|
-
|
|
1022
|
-
const validator = schemaShield.compile(schema);
|
|
1023
|
-
|
|
1024
|
-
const invalidData = {
|
|
1025
|
-
description: "Say hello to the bot.",
|
|
1026
|
-
shouldLoadDb: false,
|
|
1027
|
-
enableNetConnectFor: [],
|
|
1028
|
-
params: {
|
|
1029
|
-
color: {
|
|
1030
|
-
type: "string",
|
|
1031
|
-
// description: "The color of the text", // Missing description on purpose
|
|
1032
|
-
default: "red"
|
|
1033
|
-
}
|
|
1034
|
-
},
|
|
1035
|
-
run: "run"
|
|
1036
|
-
};
|
|
1037
|
-
|
|
1038
|
-
const validationResult = validator(invalidData);
|
|
1039
|
-
|
|
1040
|
-
if (validationResult.valid) {
|
|
1041
|
-
console.log("Data is valid:", validationResult.data);
|
|
1042
|
-
} else {
|
|
1043
|
-
console.error("Validation error:", validationResult.error.message); // "Property is invalid"
|
|
1044
|
-
|
|
1045
|
-
// Get the paths to the error location in the schema and in the data
|
|
1046
|
-
const errorPaths = validationResult.error.getPath();
|
|
1047
|
-
console.error("Schema path:", errorPaths.schemaPath); // "#/properties/params/additionalProperties/required"
|
|
1048
|
-
console.error("Instance path:", errorPaths.instancePath); // "#/params/color/description"
|
|
1049
|
-
}
|
|
1050
|
-
```
|
|
1051
|
-
|
|
1052
|
-
### Get the full error chain as a tree
|
|
1053
|
-
|
|
1054
|
-
You can use the `getTree()` method to retrieve the full error chain as a tree. This method returns an ErrorTree object with the complete nested error structure, allowing you to analyze the full chain of errors that occurred during validation.
|
|
1055
|
-
|
|
1056
|
-
#### ErrorTree Signature
|
|
1057
|
-
|
|
1058
|
-
```typescript
|
|
1059
|
-
interface ErrorTree {
|
|
1060
|
-
message: string; // The error message
|
|
1061
|
-
keyword: string; // The keyword that triggered the error
|
|
1062
|
-
item?: string | number; // The final item in the path that caused the error (either a string or a number) (optional)
|
|
1063
|
-
schemaPath: string; // The JSON Pointer path to the error location in the schema
|
|
1064
|
-
instancePath: string; // The JSON Pointer path to the error location in the data
|
|
1065
|
-
data?: any; // The data that caused the error (optional)
|
|
1066
|
-
cause?: ErrorTree; // A nested ErrorTree representation of the nested error that caused the current error
|
|
1067
|
-
}
|
|
1068
|
-
```
|
|
1069
|
-
|
|
1070
|
-
**Example:**
|
|
1071
|
-
|
|
1072
|
-
```javascript
|
|
1073
|
-
import { SchemaShield } from "schema-shield";
|
|
1074
|
-
|
|
1075
|
-
const schemaShield = new SchemaShield({ failFast: false });
|
|
1076
|
-
|
|
1077
|
-
const schema = {
|
|
1078
|
-
type: "object",
|
|
1079
|
-
properties: {
|
|
1080
|
-
description: { type: "string" },
|
|
1081
|
-
shouldLoadDb: { type: "boolean" },
|
|
1082
|
-
enableNetConnectFor: { type: "array", items: { type: "string" } },
|
|
1083
|
-
params: {
|
|
1084
|
-
type: "object",
|
|
1085
|
-
additionalProperties: {
|
|
1086
|
-
type: "object",
|
|
1087
|
-
properties: {
|
|
1088
|
-
description: { type: "string" },
|
|
1089
|
-
default: { type: "string" }
|
|
1090
|
-
},
|
|
1091
|
-
required: ["description"]
|
|
1092
|
-
}
|
|
1093
|
-
},
|
|
1094
|
-
run: { type: "string" }
|
|
1095
|
-
}
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1098
|
-
const validator = schemaShield.compile(schema);
|
|
1099
|
-
|
|
1100
|
-
const invalidData = {
|
|
1101
|
-
description: "Say hello to the bot.",
|
|
1102
|
-
shouldLoadDb: false,
|
|
1103
|
-
enableNetConnectFor: [],
|
|
1104
|
-
params: {
|
|
1105
|
-
color: {
|
|
1106
|
-
type: "string",
|
|
1107
|
-
// description: "The color of the text", // Missing description on purpose
|
|
1108
|
-
default: "red"
|
|
1109
|
-
}
|
|
1110
|
-
},
|
|
1111
|
-
run: "run"
|
|
1112
|
-
};
|
|
568
|
+
});
|
|
1113
569
|
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
} else {
|
|
1119
|
-
console.error("Validation error:", validationResult.error.message); // "Property is invalid"
|
|
1120
|
-
|
|
1121
|
-
// Get the full error chain as a tree
|
|
1122
|
-
const errorTree = validationResult.error.getTree();
|
|
1123
|
-
console.error(errorTree);
|
|
1124
|
-
|
|
1125
|
-
/*
|
|
1126
|
-
{
|
|
1127
|
-
message: "Property is invalid",
|
|
1128
|
-
keyword: "properties",
|
|
1129
|
-
item: "params",
|
|
1130
|
-
schemaPath: "#/properties/params",
|
|
1131
|
-
instancePath: "#/params",
|
|
1132
|
-
data: { color: { type: "string", default: "red" } },
|
|
1133
|
-
cause: {
|
|
1134
|
-
message: "Additional properties are invalid",
|
|
1135
|
-
keyword: "additionalProperties",
|
|
1136
|
-
item: "color",
|
|
1137
|
-
schemaPath: "#/properties/params/additionalProperties",
|
|
1138
|
-
instancePath: "#/params/color",
|
|
1139
|
-
data: { type: "string", default: "red" },
|
|
1140
|
-
cause: {
|
|
1141
|
-
message: "Required property is missing",
|
|
1142
|
-
keyword: "required",
|
|
1143
|
-
item: "description",
|
|
1144
|
-
schemaPath: "#/properties/params/additionalProperties/required",
|
|
1145
|
-
instancePath: "#/params/color/description",
|
|
1146
|
-
data: undefined
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
*/
|
|
1151
|
-
}
|
|
570
|
+
const validator = schemaShield.compile({
|
|
571
|
+
type: "number",
|
|
572
|
+
divisibleBy: 5
|
|
573
|
+
});
|
|
1152
574
|
```
|
|
1153
575
|
|
|
1154
|
-
The `
|
|
576
|
+
The `instance` argument provides access to types, formats, and keywords registered on the active `SchemaShield` instance through `getType()`, `getFormat()`, and `getKeyword()`.
|
|
1155
577
|
|
|
1156
|
-
|
|
578
|
+
When `failFast: true`, `defineError()` returns `true`. When `failFast: false`, it returns a `ValidationError`. A custom keyword that returns `true` directly produces `error: true` in either mode, so use `defineError()` when you need detailed errors.
|
|
1157
579
|
|
|
1158
|
-
|
|
580
|
+
## Supported Formats and Compatibility
|
|
1159
581
|
|
|
1160
|
-
|
|
1161
|
-
import { SchemaShield } from "schema-shield";
|
|
582
|
+
SchemaShield supports draft-06 and draft-07 JSON Schema validation and includes built-in validators for:
|
|
1162
583
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
enableNetConnectFor: { type: "array", items: { type: "string" } },
|
|
1171
|
-
params: {
|
|
1172
|
-
type: "object",
|
|
1173
|
-
additionalProperties: {
|
|
1174
|
-
type: "object",
|
|
1175
|
-
properties: {
|
|
1176
|
-
description: { type: "string" },
|
|
1177
|
-
default: { type: "string" }
|
|
1178
|
-
},
|
|
1179
|
-
required: ["description"]
|
|
1180
|
-
}
|
|
1181
|
-
},
|
|
1182
|
-
run: { type: "string" }
|
|
1183
|
-
}
|
|
1184
|
-
};
|
|
584
|
+
- **Date and time:** `date`, `time`, `date-time`, `duration`
|
|
585
|
+
- **Email:** `email`, `idn-email`
|
|
586
|
+
- **Hostnames:** `hostname`, `idn-hostname`
|
|
587
|
+
- **IP addresses:** `ipv4`, `ipv6`
|
|
588
|
+
- **Resource identifiers:** `uuid`, `uri`, `uri-reference`, `uri-template`, `iri`, `iri-reference`
|
|
589
|
+
- **JSON Pointers:** `json-pointer`, `relative-json-pointer`
|
|
590
|
+
- **Regular expressions:** `regex`
|
|
1185
591
|
|
|
1186
|
-
|
|
592
|
+
Remote references are intentionally outside the offline execution model described above.
|
|
1187
593
|
|
|
1188
|
-
|
|
1189
|
-
description: "Say hello to the bot.",
|
|
1190
|
-
shouldLoadDb: false,
|
|
1191
|
-
enableNetConnectFor: [],
|
|
1192
|
-
params: {
|
|
1193
|
-
color: {
|
|
1194
|
-
type: "string",
|
|
1195
|
-
// description: "The color of the text", // Missing description on purpose
|
|
1196
|
-
default: "red"
|
|
1197
|
-
}
|
|
1198
|
-
},
|
|
1199
|
-
run: "run"
|
|
1200
|
-
};
|
|
594
|
+
Any built-in format can be replaced, and new formats can be added with `schemaShield.addFormat()`.
|
|
1201
595
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
if (validationResult.valid) {
|
|
1205
|
-
console.log("Data is valid:", validationResult.data);
|
|
1206
|
-
} else {
|
|
1207
|
-
console.error("Validation error:", validationResult.error.message); // "Property is invalid"
|
|
1208
|
-
|
|
1209
|
-
// Get the root cause of the error
|
|
1210
|
-
const errorCause = validationResult.error.getCause();
|
|
1211
|
-
console.error("Root cause:", errorCause.message); // "Required property is missing"
|
|
1212
|
-
console.error("Schema path:", errorCause.schemaPath); // "#/properties/params/additionalProperties/required"
|
|
1213
|
-
console.error("Instance path:", errorCause.instancePath); // "#/params/color/description"
|
|
1214
|
-
console.error("Error data:", errorCause.data); // undefined
|
|
1215
|
-
console.error("Error schema:", errorCause.schema); // ["description"]
|
|
1216
|
-
console.error("Error keyword:", errorCause.keyword); // "required"
|
|
1217
|
-
}
|
|
1218
|
-
```
|
|
596
|
+
### TypeScript
|
|
1219
597
|
|
|
1220
|
-
|
|
598
|
+
Type declarations are included in the package. CJS, ESM, browser, and declaration package smokes have been verified.
|
|
1221
599
|
|
|
1222
|
-
|
|
600
|
+
## Immutable Mode and Known Limitations
|
|
1223
601
|
|
|
1224
|
-
|
|
602
|
+
### Immutable Mode
|
|
1225
603
|
|
|
1226
|
-
|
|
604
|
+
SchemaShield can apply defaults, and application-provided custom keywords can modify data. Enable immutable mode when the original input must remain unchanged:
|
|
1227
605
|
|
|
1228
606
|
```javascript
|
|
1229
607
|
const schemaShield = new SchemaShield({ immutable: true });
|
|
1230
608
|
```
|
|
1231
609
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
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.
|
|
1235
|
-
|
|
1236
|
-
## TypeScript Support
|
|
1237
|
-
|
|
1238
|
-
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.
|
|
1239
|
-
|
|
1240
|
-
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.
|
|
1241
|
-
|
|
1242
|
-
## Known Limitations
|
|
1243
|
-
|
|
1244
|
-
SchemaShield is optimized for local execution and strict security.
|
|
1245
|
-
|
|
1246
|
-
### 1. Dynamic ID Scope Resolution (Scope Alteration)
|
|
1247
|
-
|
|
1248
|
-
SchemaShield resolves references based on static JSON Pointers and unique IDs. It does not support changing the resolution base URI dynamically based on nested `$id` properties within sub-schemas.
|
|
610
|
+
Immutable mode creates a deep copy before validation. This preserves ordinary JSON-compatible data, but cloning may not reproduce instantiated classes or other complex objects accurately. Applications that validate such values can manage cloning explicitly.
|
|
1249
611
|
|
|
1250
|
-
|
|
612
|
+
Leave immutable mode disabled when input preservation is unnecessary and validation performance is the priority.
|
|
1251
613
|
|
|
1252
|
-
###
|
|
614
|
+
### Remote References
|
|
1253
615
|
|
|
1254
|
-
SchemaShield
|
|
616
|
+
SchemaShield does not retrieve remote references and does not provide an external registry or multi-document bundle API. Every referenced target must form part of the single root schema supplied to `compile()`.
|
|
1255
617
|
|
|
1256
|
-
|
|
618
|
+
### Structural Equality
|
|
1257
619
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
For keywords like `enum`, `const`, and `uniqueItems`, SchemaShield prioritizes exact structural comparisons to preserve predictable behavior.
|
|
1261
|
-
|
|
1262
|
-
- **Impact:** For very large arrays or enums with many complex objects, this conservative path can be slower than aggressive hashing strategies.
|
|
1263
|
-
- **Future Option:** An opt-in aggressive mode based on structural hashing/bucketing could improve throughput in those extreme cases, but it is intentionally not enabled by default to avoid edge-case semantic divergence.
|
|
620
|
+
`enum`, `const`, and `uniqueItems` use exact structural comparison for predictable semantics. Very large arrays or enums containing complex objects can take longer to compare than strategies based on aggressive structural hashing.
|
|
1264
621
|
|
|
1265
622
|
## Testing
|
|
1266
623
|
|
|
1267
|
-
SchemaShield
|
|
624
|
+
SchemaShield is tested against the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) and the package's own functional and distribution checks.
|
|
1268
625
|
|
|
1269
|
-
|
|
626
|
+
The latest observed functional suite completed with **1,635 passing** and **14 pending**. Package smokes for CJS, ESM, browser, and TypeScript declarations also passed.
|
|
1270
627
|
|
|
1271
628
|
```bash
|
|
1272
629
|
npm test
|
|
1273
|
-
# or
|
|
1274
|
-
npm run test:dev # for development
|
|
1275
630
|
```
|
|
1276
631
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
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.
|
|
1280
|
-
|
|
1281
|
-
If you are interested in contributing, please follow these steps:
|
|
1282
|
-
|
|
1283
|
-
- **Fork the repository:** Fork the SchemaShield repository on GitHub and clone it to your local machine.
|
|
1284
|
-
|
|
1285
|
-
- **Create a feature branch:** Create a new branch for your feature or bugfix. Make sure to give it a descriptive name.
|
|
632
|
+
For development:
|
|
1286
633
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
634
|
+
```bash
|
|
635
|
+
npm run dev:test
|
|
636
|
+
```
|
|
1290
637
|
|
|
1291
|
-
|
|
638
|
+
## Contribute
|
|
1292
639
|
|
|
1293
|
-
|
|
640
|
+
SchemaShield is open source. Contributions that improve validation behavior, developer experience, documentation, compatibility, or performance are welcome.
|
|
1294
641
|
|
|
1295
|
-
|
|
642
|
+
Before opening a pull request:
|
|
1296
643
|
|
|
1297
|
-
|
|
644
|
+
1. Add or update the relevant tests and documentation.
|
|
645
|
+
2. Run the functional suite.
|
|
646
|
+
3. Check that the change does not introduce a performance regression.
|
|
647
|
+
4. Explain the behavior change and its motivation in the pull request.
|
|
1298
648
|
|
|
1299
649
|
## Acknowledgments
|
|
1300
650
|
|
|
1301
|
-
- **
|
|
1302
|
-
|
|
1303
|
-
- **@exodus/schemasafe**: Your interpreter-first approach validates that security-first architectures can still deliver strong performance. Competing with you makes us better.
|
|
651
|
+
- **AJV:** The performance reference for JSON Schema validation. Its JIT architecture sets the raw-throughput bar for the ecosystem.
|
|
652
|
+
- **@exodus/schemasafe:** A strong interpreter-first validator that demonstrates the value of security-focused validation architectures. Competing with it makes SchemaShield better.
|
|
1304
653
|
|
|
1305
654
|
## Legal
|
|
1306
655
|
|
|
1307
656
|
Author: [Masquerade Circus](http://masquerade-circus.net).
|
|
1308
|
-
|
|
657
|
+
|
|
658
|
+
License: [Apache-2.0](https://opensource.org/licenses/Apache-2.0).
|