json-schema-compatibility-checker 1.0.10 → 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 +158 -83
- package/dist/cjs/condition-resolver.d.ts +6 -6
- package/dist/cjs/condition-resolver.js +1 -1
- package/dist/cjs/condition-resolver.js.map +1 -1
- package/dist/cjs/constraint-validator.d.ts +21 -0
- package/dist/cjs/constraint-validator.js +2 -0
- package/dist/cjs/constraint-validator.js.map +1 -0
- package/dist/cjs/data-narrowing.d.ts +4 -0
- package/dist/cjs/data-narrowing.js +1 -1
- package/dist/cjs/data-narrowing.js.map +1 -1
- package/dist/cjs/format-validator.d.ts +40 -40
- package/dist/cjs/format-validator.js.map +1 -1
- package/dist/cjs/formatter.d.ts +4 -4
- package/dist/cjs/formatter.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/json-schema-compatibility-checker.d.ts +58 -36
- package/dist/cjs/json-schema-compatibility-checker.js +1 -1
- package/dist/cjs/json-schema-compatibility-checker.js.map +1 -1
- package/dist/cjs/merge-engine.d.ts +10 -10
- package/dist/cjs/merge-engine.js +1 -1
- package/dist/cjs/merge-engine.js.map +1 -1
- package/dist/cjs/normalizer.d.ts +15 -15
- package/dist/cjs/normalizer.js +1 -1
- package/dist/cjs/normalizer.js.map +1 -1
- package/dist/cjs/pattern-subset.d.ts +33 -33
- package/dist/cjs/pattern-subset.js.map +1 -1
- package/dist/cjs/runtime-validator.d.ts +30 -0
- package/dist/cjs/runtime-validator.js +2 -0
- package/dist/cjs/runtime-validator.js.map +1 -0
- package/dist/cjs/semantic-errors.d.ts +7 -7
- package/dist/cjs/semantic-errors.js +1 -1
- package/dist/cjs/semantic-errors.js.map +1 -1
- package/dist/cjs/subset-checker.d.ts +37 -44
- package/dist/cjs/subset-checker.js +1 -1
- package/dist/cjs/subset-checker.js.map +1 -1
- package/dist/cjs/types.d.ts +89 -20
- package/dist/cjs/utils.d.ts +37 -20
- package/dist/cjs/utils.js +1 -1
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/condition-resolver.d.ts +6 -6
- package/dist/esm/condition-resolver.js +1 -1
- package/dist/esm/condition-resolver.js.map +1 -1
- package/dist/esm/constraint-validator.d.ts +21 -0
- package/dist/esm/constraint-validator.js +2 -0
- package/dist/esm/constraint-validator.js.map +1 -0
- package/dist/esm/data-narrowing.d.ts +4 -0
- package/dist/esm/data-narrowing.js +1 -1
- package/dist/esm/data-narrowing.js.map +1 -1
- package/dist/esm/format-validator.d.ts +40 -40
- package/dist/esm/format-validator.js.map +1 -1
- package/dist/esm/formatter.d.ts +4 -4
- package/dist/esm/formatter.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/json-schema-compatibility-checker.d.ts +58 -36
- package/dist/esm/json-schema-compatibility-checker.js +1 -1
- package/dist/esm/json-schema-compatibility-checker.js.map +1 -1
- package/dist/esm/merge-engine.d.ts +10 -10
- package/dist/esm/merge-engine.js +1 -1
- package/dist/esm/merge-engine.js.map +1 -1
- package/dist/esm/normalizer.d.ts +15 -15
- package/dist/esm/normalizer.js +1 -1
- package/dist/esm/normalizer.js.map +1 -1
- package/dist/esm/pattern-subset.d.ts +33 -33
- package/dist/esm/pattern-subset.js.map +1 -1
- package/dist/esm/runtime-validator.d.ts +30 -0
- package/dist/esm/runtime-validator.js +2 -0
- package/dist/esm/runtime-validator.js.map +1 -0
- package/dist/esm/semantic-errors.d.ts +7 -7
- package/dist/esm/semantic-errors.js +1 -1
- package/dist/esm/semantic-errors.js.map +1 -1
- package/dist/esm/subset-checker.d.ts +37 -44
- package/dist/esm/subset-checker.js +1 -1
- package/dist/esm/subset-checker.js.map +1 -1
- package/dist/esm/types.d.ts +89 -20
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/utils.d.ts +37 -20
- package/dist/esm/utils.js +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,70 +1,73 @@
|
|
|
1
1
|
# JSON Schema Compatibility Checker
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Check structural compatibility between JSON Schemas (Draft-07) using a set-theoretic intersection approach.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Sommaire
|
|
8
8
|
|
|
9
9
|
- [Introduction](#introduction)
|
|
10
|
-
- [
|
|
10
|
+
- [Mathematical Principle](#mathematical-principle)
|
|
11
11
|
- [Installation](#installation)
|
|
12
|
-
- [
|
|
12
|
+
- [Quick Start](#quick-start)
|
|
13
13
|
- [API Reference](#api-reference)
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
14
|
+
- [Runtime Validation & Custom Constraints](#runtime-validation--custom-constraints)
|
|
15
|
+
- [Full Documentation](#-full-documentation)
|
|
16
|
+
- [Known Limitations](#known-limitations)
|
|
17
|
+
- [License](#license)
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
20
21
|
## Introduction
|
|
21
22
|
|
|
22
|
-
**JSON Schema Compatibility Checker**
|
|
23
|
+
**JSON Schema Compatibility Checker** is a TypeScript library that checks structural compatibility between two JSON Schemas (Draft-07).
|
|
23
24
|
|
|
24
|
-
###
|
|
25
|
+
### Why this library?
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
In workflow systems, node orchestration, or API integration, a recurring question is:
|
|
27
28
|
|
|
28
|
-
> *"
|
|
29
|
+
> *"Is the output of component A compatible with the input of component B?"*
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
In other words: **will every value produced by A be accepted by B?**
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
This library answers that question by checking if one schema is a **subset** of another, with detailed structural diagnostics when incompatible.
|
|
33
34
|
|
|
34
|
-
###
|
|
35
|
+
### What it does
|
|
35
36
|
|
|
36
|
-
- ✅
|
|
37
|
-
- ✅
|
|
38
|
-
- ✅
|
|
39
|
-
- ✅
|
|
40
|
-
- ✅
|
|
41
|
-
- ✅
|
|
42
|
-
- ✅
|
|
43
|
-
- ✅
|
|
44
|
-
- ✅
|
|
37
|
+
- ✅ Checks if a schema is a subset of another (`sub ⊆ sup`)
|
|
38
|
+
- ✅ Produces detailed diagnostics with structural differences
|
|
39
|
+
- ✅ Computes the intersection of two schemas (`allOf` merge)
|
|
40
|
+
- ✅ Accumulates schemas sequentially via deep spread (`overlay`)
|
|
41
|
+
- ✅ Resolves `if/then/else` conditions with discriminant data
|
|
42
|
+
- ✅ Handles `anyOf`, `oneOf`, `not`, `format`, `pattern`, `dependencies`, etc.
|
|
43
|
+
- ✅ Handles `oneOf`/`anyOf` nested inside object properties and array items
|
|
44
|
+
- ✅ Compares regex patterns via sampling
|
|
45
|
+
- ✅ Validates runtime data against resolved schemas (via [AJV](https://ajv.js.org/))
|
|
46
|
+
- ✅ Supports custom `constraints` keyword with user-provided validators
|
|
47
|
+
- ✅ Provides human-readable formatting of results for debugging
|
|
45
48
|
|
|
46
49
|
---
|
|
47
50
|
|
|
48
|
-
##
|
|
51
|
+
## Mathematical Principle
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
The core of the library relies on a simple set-theoretic principle:
|
|
51
54
|
|
|
52
55
|
```
|
|
53
56
|
A ⊆ B ⟺ A ∩ B ≡ A
|
|
54
57
|
```
|
|
55
58
|
|
|
56
|
-
**
|
|
59
|
+
**A schema A is a subset of B if and only if the intersection of A and B is structurally identical to A.**
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
In JSON Schema terms:
|
|
59
62
|
|
|
60
|
-
|
|
|
63
|
+
| Mathematical concept | JSON Schema translation |
|
|
61
64
|
|---|---|
|
|
62
|
-
| `A ∩ B` | `allOf([A, B])`
|
|
63
|
-
| `≡` (
|
|
65
|
+
| `A ∩ B` | `allOf([A, B])` resolved via merge |
|
|
66
|
+
| `≡` (equivalence) | Deep structural comparison |
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
If after the merge (intersection) the result is identical to the original schema `A`, then `A` was not "restricted" by `B` — meaning `A` is already contained within `B`.
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
If the merge produces a result different from `A`, the structural differences constitute the **diagnostic** of the incompatibility.
|
|
68
71
|
|
|
69
72
|
---
|
|
70
73
|
|
|
@@ -74,20 +77,29 @@ Si le merge produit un résultat différent de `A`, les différences structurell
|
|
|
74
77
|
bun add json-schema-compatibility-checker
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
> **
|
|
80
|
+
> **Prerequisites**: TypeScript ≥ 5, ESM-compatible runtime (Bun, Node 18+).
|
|
81
|
+
|
|
82
|
+
### Runtime dependencies
|
|
83
|
+
|
|
84
|
+
| Package | Role | Size impact |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `@x0k/json-schema-merge` | Schema intersection (`allOf` merge) | lightweight |
|
|
87
|
+
| `ajv` + `ajv-formats` | Runtime JSON Schema validation (condition evaluation, data validation) | ~250KB |
|
|
88
|
+
| `class-validator` | Format validation helpers | lightweight |
|
|
89
|
+
| `randexp` | Regex pattern sampling for subset analysis | lightweight |
|
|
78
90
|
|
|
79
91
|
---
|
|
80
92
|
|
|
81
|
-
##
|
|
93
|
+
## Quick Start
|
|
82
94
|
|
|
83
|
-
|
|
95
|
+
The simplest example: check if a strict schema is compatible with a more permissive one.
|
|
84
96
|
|
|
85
97
|
```ts
|
|
86
98
|
import { JsonSchemaCompatibilityChecker } from "json-schema-compatibility-checker";
|
|
87
99
|
|
|
88
100
|
const checker = new JsonSchemaCompatibilityChecker();
|
|
89
101
|
|
|
90
|
-
//
|
|
102
|
+
// Strict schema: requires name AND age
|
|
91
103
|
const strict = {
|
|
92
104
|
type: "object",
|
|
93
105
|
properties: {
|
|
@@ -97,7 +109,7 @@ const strict = {
|
|
|
97
109
|
required: ["name", "age"],
|
|
98
110
|
};
|
|
99
111
|
|
|
100
|
-
//
|
|
112
|
+
// Permissive schema: requires only name
|
|
101
113
|
const loose = {
|
|
102
114
|
type: "object",
|
|
103
115
|
properties: {
|
|
@@ -106,12 +118,12 @@ const loose = {
|
|
|
106
118
|
required: ["name"],
|
|
107
119
|
};
|
|
108
120
|
|
|
109
|
-
//
|
|
121
|
+
// Is every value valid for strict also valid for loose?
|
|
110
122
|
console.log(checker.isSubset(strict, loose)); // true ✅
|
|
111
123
|
|
|
112
|
-
//
|
|
124
|
+
// Is the reverse true?
|
|
113
125
|
console.log(checker.isSubset(loose, strict)); // false ❌
|
|
114
|
-
// →
|
|
126
|
+
// → An object { name: "Alice" } (no age) is valid for loose but not for strict
|
|
115
127
|
```
|
|
116
128
|
|
|
117
129
|
---
|
|
@@ -120,26 +132,27 @@ console.log(checker.isSubset(loose, strict)); // false ❌
|
|
|
120
132
|
|
|
121
133
|
### `JsonSchemaCompatibilityChecker`
|
|
122
134
|
|
|
123
|
-
|
|
135
|
+
All compatibility checking methods are exposed by the `JsonSchemaCompatibilityChecker` class.
|
|
124
136
|
|
|
125
137
|
```ts
|
|
126
138
|
const checker = new JsonSchemaCompatibilityChecker();
|
|
127
139
|
```
|
|
128
140
|
|
|
129
|
-
|
|
|
141
|
+
| Method | Description | Returns |
|
|
130
142
|
|---|---|---|
|
|
131
|
-
| `isSubset(sub, sup)` |
|
|
132
|
-
| `check(sub, sup)` |
|
|
133
|
-
| `check(sub, sup, options)` |
|
|
134
|
-
| `isEqual(a, b)` |
|
|
135
|
-
| `intersect(a, b)` | Intersection
|
|
136
|
-
| `resolveConditions(schema, data)` |
|
|
137
|
-
| `normalize(schema)` |
|
|
138
|
-
| `formatResult(label, result)` |
|
|
143
|
+
| `isSubset(sub, sup)` | Checks if `sub ⊆ sup` | `boolean` |
|
|
144
|
+
| `check(sub, sup)` | Checks with detailed diagnostics | `SubsetResult` |
|
|
145
|
+
| `check(sub, sup, options)` | Checks with `if/then/else` condition resolution and runtime validation | `ResolvedSubsetResult` |
|
|
146
|
+
| `isEqual(a, b)` | Structural equality after normalization | `boolean` |
|
|
147
|
+
| `intersect(a, b)` | Intersection of two schemas | `JSONSchema7Definition \| null` |
|
|
148
|
+
| `resolveConditions(schema, data)` | Resolves `if/then/else` with runtime data | `ResolvedConditionResult` |
|
|
149
|
+
| `normalize(schema)` | Normalizes a schema (infers types, resolves double negation, canonicalizes constraints) | `JSONSchema7Definition` |
|
|
150
|
+
| `formatResult(label, result)` | Formats a result for debug output | `string` |
|
|
151
|
+
| `clearValidatorCache()` | Clears the AJV compiled validator caches (useful for long-running processes or tests) | `void` |
|
|
139
152
|
|
|
140
153
|
### `MergeEngine`
|
|
141
154
|
|
|
142
|
-
|
|
155
|
+
Low-level schema operations: intersection (`allOf` merge) and overlay (sequential deep spread).
|
|
143
156
|
|
|
144
157
|
```ts
|
|
145
158
|
import { MergeEngine } from "json-schema-compatibility-checker";
|
|
@@ -147,15 +160,15 @@ import { MergeEngine } from "json-schema-compatibility-checker";
|
|
|
147
160
|
const engine = new MergeEngine();
|
|
148
161
|
```
|
|
149
162
|
|
|
150
|
-
|
|
|
163
|
+
| Method | Description | Returns |
|
|
151
164
|
|---|---|---|
|
|
152
|
-
| `merge(a, b)` | Intersection `allOf([a, b])` —
|
|
153
|
-
| `mergeOrThrow(a, b)` |
|
|
154
|
-
| `overlay(base, override)` |
|
|
155
|
-
| `compare(a, b)` |
|
|
156
|
-
| `isEqual(a, b)` |
|
|
165
|
+
| `merge(a, b)` | Intersection `allOf([a, b])` — returns `null` if incompatible | `JSONSchema7Definition \| null` |
|
|
166
|
+
| `mergeOrThrow(a, b)` | Like `merge`, but throws if incompatible | `JSONSchema7Definition` |
|
|
167
|
+
| `overlay(base, override)` | Sequential deep spread — last writer wins per property | `JSONSchema7Definition` |
|
|
168
|
+
| `compare(a, b)` | Structural comparison (0 = identical) | `number` |
|
|
169
|
+
| `isEqual(a, b)` | Structural equality | `boolean` |
|
|
157
170
|
|
|
158
|
-
**
|
|
171
|
+
**Quick example — `check` with diagnostics:**
|
|
159
172
|
|
|
160
173
|
```ts
|
|
161
174
|
const result = checker.check(
|
|
@@ -168,78 +181,140 @@ console.log(result.errors);
|
|
|
168
181
|
// [{ key: "age", expected: "number", received: "undefined" }]
|
|
169
182
|
```
|
|
170
183
|
|
|
171
|
-
**
|
|
184
|
+
**Quick example — condition resolution:**
|
|
172
185
|
|
|
173
186
|
```ts
|
|
174
187
|
const result = checker.check(sub, conditionalSup, {
|
|
175
|
-
|
|
188
|
+
data: { kind: "text" },
|
|
176
189
|
});
|
|
177
190
|
console.log(result.isSubset); // true ✅
|
|
178
191
|
console.log(result.resolvedSup.branch); // "then"
|
|
179
192
|
```
|
|
180
193
|
|
|
181
|
-
**
|
|
194
|
+
**Quick example — `overlay` for sequential accumulation:**
|
|
182
195
|
|
|
183
196
|
```ts
|
|
184
197
|
import { MergeEngine } from "json-schema-compatibility-checker";
|
|
185
198
|
|
|
186
199
|
const engine = new MergeEngine();
|
|
187
200
|
|
|
188
|
-
// Node1
|
|
201
|
+
// Node1 produces accountId with enum
|
|
189
202
|
const node1Output = {
|
|
190
203
|
type: "object",
|
|
191
204
|
properties: { accountId: { type: "string", enum: ["a", "b"] } },
|
|
192
205
|
required: ["accountId"],
|
|
193
206
|
};
|
|
194
207
|
|
|
195
|
-
// Node2
|
|
208
|
+
// Node2 redefines accountId as a simple string (wider)
|
|
196
209
|
const node2Output = {
|
|
197
210
|
type: "object",
|
|
198
211
|
properties: { accountId: { type: "string" } },
|
|
199
212
|
required: ["accountId"],
|
|
200
213
|
};
|
|
201
214
|
|
|
202
|
-
// ❌ merge (intersection)
|
|
215
|
+
// ❌ merge (intersection): keeps the enum — WRONG for a sequential pipeline
|
|
203
216
|
engine.merge(node1Output, node2Output);
|
|
204
217
|
// → { ..., properties: { accountId: { type: "string", enum: ["a", "b"] } } }
|
|
205
218
|
|
|
206
|
-
// ✅ overlay (deep spread)
|
|
219
|
+
// ✅ overlay (deep spread): last writer wins — CORRECT
|
|
207
220
|
engine.overlay(node1Output, node2Output);
|
|
208
221
|
// → { ..., properties: { accountId: { type: "string" } } }
|
|
209
222
|
```
|
|
210
223
|
|
|
211
|
-
👉
|
|
224
|
+
👉 For full documentation of every method with examples, see the **[API Reference](./docs/api-reference.md)**.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Runtime Validation & Custom Constraints
|
|
229
|
+
|
|
230
|
+
### Runtime data validation
|
|
231
|
+
|
|
232
|
+
When `check()` is called with `{ data }`, the library uses [AJV](https://ajv.js.org/) (JSON Schema validator) to:
|
|
233
|
+
|
|
234
|
+
1. **Resolve `if/then/else` conditions** — evaluates the `if` schema against the data to determine which branch applies
|
|
235
|
+
2. **Validate data against both resolved schemas** — catches data-level violations (wrong format, out of range, etc.)
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const result = checker.check(sub, sup, {
|
|
239
|
+
data: { kind: "email", value: "test@example.com" },
|
|
240
|
+
});
|
|
241
|
+
// result.isSubset — structural compatibility
|
|
242
|
+
// result.errors — includes runtime validation errors prefixed with $sub / $sup
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Custom `constraints` keyword
|
|
246
|
+
|
|
247
|
+
The library extends JSON Schema with a custom `constraints` keyword for domain-specific validation rules that go beyond what JSON Schema can express (e.g. "is a valid UUID", "belongs to scope", "minimum age"):
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { JsonSchemaCompatibilityChecker } from "json-schema-compatibility-checker";
|
|
251
|
+
|
|
252
|
+
const checker = new JsonSchemaCompatibilityChecker({
|
|
253
|
+
constraints: {
|
|
254
|
+
IsUuid: (value) => ({
|
|
255
|
+
valid: typeof value === "string" && /^[0-9a-f]{8}-/.test(value),
|
|
256
|
+
message: "Value must be a valid UUID",
|
|
257
|
+
}),
|
|
258
|
+
MinAge: (value, params) => ({
|
|
259
|
+
valid: typeof value === "number" && value >= (params?.min ?? 0),
|
|
260
|
+
message: `Value must be at least ${params?.min}`,
|
|
261
|
+
}),
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Constraints in schemas
|
|
266
|
+
const sub = {
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {
|
|
269
|
+
id: { type: "string", constraints: ["IsUuid"] },
|
|
270
|
+
age: { type: "number", constraints: [{ name: "MinAge", params: { min: 18 } }] },
|
|
271
|
+
},
|
|
272
|
+
required: ["id", "age"],
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Static subset checking works with constraints (structural comparison)
|
|
276
|
+
checker.isSubset(sub, sup); // compares constraints via deepEqual after merge
|
|
277
|
+
|
|
278
|
+
// Runtime validation evaluates constraints against actual data
|
|
279
|
+
checker.check(sub, sup, { data: { id: "not-a-uuid", age: 15 } });
|
|
280
|
+
// → errors include constraint violations
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Constraints are handled at three levels:
|
|
284
|
+
- **Structurally**: the merge engine unions them (intersection semantics — `allOf`)
|
|
285
|
+
- **Statically**: the subset checker compares them via `deepEqual` after merge
|
|
286
|
+
- **At runtime**: the constraint validator evaluates them against concrete data
|
|
212
287
|
|
|
213
288
|
---
|
|
214
289
|
|
|
215
|
-
## 📖 Documentation
|
|
290
|
+
## 📖 Full Documentation
|
|
216
291
|
|
|
217
292
|
| Page | Description |
|
|
218
293
|
|---|---|
|
|
219
|
-
| **[
|
|
220
|
-
| **[Guide
|
|
221
|
-
| **[
|
|
222
|
-
| **[
|
|
223
|
-
| **[Types
|
|
224
|
-
| **[Limitations
|
|
225
|
-
| **[Architecture
|
|
294
|
+
| **[API Reference](./docs/api-reference.md)** | Detailed documentation of every method (`JsonSchemaCompatibilityChecker` + `MergeEngine`) with examples |
|
|
295
|
+
| **[Features Guide](./docs/features-guide.md)** | Complete feature tour: types, `required`, numeric constraints, `enum`/`const`, `anyOf`/`oneOf`, `not`, `format`, `pattern`, `if/then/else` conditions, `allOf`, custom `constraints`... |
|
|
296
|
+
| **[Utility Functions](./docs/utilities.md)** | `isPatternSubset`, `arePatternsEquivalent`, `isTrivialPattern` |
|
|
297
|
+
| **[Use Cases](./docs/use-cases.md)** | Node connection, sequential pipeline (overlay), API response validation, discriminated unions, conditional forms |
|
|
298
|
+
| **[Exported Types](./docs/types.md)** | `SubsetResult`, `SchemaError`, `ResolvedConditionResult`, `ResolvedSubsetResult`, `CheckRuntimeOptions`, `ConstraintValidator`, `CheckerOptions` |
|
|
299
|
+
| **[Known Limitations](./docs/limitations.md)** | Cross-keyword constraints, `oneOf` exclusivity, probabilistic patterns, `$ref` not supported |
|
|
300
|
+
| **[Internal Architecture](./docs/architecture.md)** | Module diagram, verification flow, merge vs overlay, dependencies |
|
|
226
301
|
|
|
227
302
|
---
|
|
228
303
|
|
|
229
|
-
## Limitations
|
|
304
|
+
## Known Limitations
|
|
230
305
|
|
|
231
|
-
- **Cross-keyword constraints
|
|
232
|
-
- **`oneOf`
|
|
233
|
-
- **
|
|
234
|
-
- **`if/then/else
|
|
235
|
-
- **`$ref
|
|
236
|
-
- **`patternProperties
|
|
237
|
-
- **Nested branching fallback
|
|
306
|
+
- **Cross-keyword constraints**: `exclusiveMinimum` vs `minimum` comparison may produce false negatives (structural limitation)
|
|
307
|
+
- **`oneOf` exclusivity**: treated like `anyOf` — semantic exclusivity is not verified
|
|
308
|
+
- **Regex patterns**: probabilistic approach via sampling (200 samples), not a formal proof
|
|
309
|
+
- **`if/then/else`**: requires discriminant data via `check(sub, sup, { data })`
|
|
310
|
+
- **`$ref`**: not supported — schemas must be pre-dereferenced
|
|
311
|
+
- **`patternProperties`**: partial support only
|
|
312
|
+
- **Nested branching fallback**: the property-by-property fallback for nested `oneOf`/`anyOf` does not check object-level keywords (`minProperties`/`maxProperties`) — those are handled by the merge when branching is not involved
|
|
238
313
|
|
|
239
|
-
👉
|
|
314
|
+
👉 Details and examples in **[Known Limitations](./docs/limitations.md)**.
|
|
240
315
|
|
|
241
316
|
---
|
|
242
317
|
|
|
243
|
-
##
|
|
318
|
+
## License
|
|
244
319
|
|
|
245
320
|
MIT
|
|
@@ -2,12 +2,12 @@ import type { JSONSchema7 } from "json-schema";
|
|
|
2
2
|
import type { MergeEngine } from "./merge-engine.js";
|
|
3
3
|
import type { ResolvedConditionResult } from "./types.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Resolves `if/then/else` in a schema by evaluating the `if` against
|
|
6
|
+
* partial data (discriminants).
|
|
7
7
|
*
|
|
8
|
-
* @param schema
|
|
9
|
-
* @param data
|
|
10
|
-
* @param engine
|
|
8
|
+
* @param schema The schema potentially containing if/then/else
|
|
9
|
+
* @param data Partial data used to evaluate the conditions
|
|
10
|
+
* @param engine The MergeEngine for merging branches
|
|
11
11
|
*
|
|
12
12
|
* @example
|
|
13
13
|
* ```ts
|
|
@@ -20,7 +20,7 @@ import type { ResolvedConditionResult } from "./types.js";
|
|
|
20
20
|
* };
|
|
21
21
|
*
|
|
22
22
|
* const { resolved } = resolveConditions(form, { accountType: "business" }, engine);
|
|
23
|
-
* // → resolved
|
|
23
|
+
* // → resolved no longer has if/then/else, but has required: ["companyName"]
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
26
|
export declare function resolveConditions(schema: JSONSchema7, data: Record<string, unknown>, engine: MergeEngine): ResolvedConditionResult;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});Object.defineProperty(exports,"resolveConditions",{enumerable:true,get:function(){return resolveConditions}});const _formatvalidator=require("./format-validator.js");const _normalizer=require("./normalizer.js");const _utils=require("./utils.js");const SPECIAL_MERGE_KEYS=new Set(["required","properties","dependencies"]);const SUB_SCHEMA_KEYS=new Set(["additionalProperties","items","contains","propertyNames","not"]);const MIN_KEYS=new Set(["minimum","exclusiveMinimum","minLength","minItems","minProperties"]);const MAX_KEYS=new Set(["maximum","exclusiveMaximum","maxLength","maxItems","maxProperties"]);function matchesType(value,type){if(type===undefined)return true;const types=Array.isArray(type)?type:[type];const actualType=(0,_normalizer.inferType)(value);return types.some(t=>t===actualType||t==="number"&&actualType==="integer")}function evaluateNumericConstraints(value,prop){if(prop.minimum!==undefined&&!(value>=prop.minimum))return false;if(prop.maximum!==undefined&&!(value<=prop.maximum))return false;if(prop.exclusiveMinimum!==undefined&&!(value>prop.exclusiveMinimum))return false;if(prop.exclusiveMaximum!==undefined&&!(value<prop.exclusiveMaximum))return false;if(prop.multipleOf!==undefined&&value%prop.multipleOf!==0)return false;return true}const patternRegexCache=new Map;function getOrCompileRegex(pattern){let regex=patternRegexCache.get(pattern);if(regex===undefined){regex=new RegExp(pattern);patternRegexCache.set(pattern,regex)}return regex}function evaluateStringConstraints(value,prop){if(prop.minLength!==undefined&&!(value.length>=prop.minLength))return false;if(prop.maxLength!==undefined&&!(value.length<=prop.maxLength))return false;if(prop.pattern!==undefined&&!getOrCompileRegex(prop.pattern).test(value))return false;return true}function evaluateArrayConstraints(value,prop){if(prop.minItems!==undefined&&!(value.length>=prop.minItems))return false;if(prop.maxItems!==undefined&&!(value.length<=prop.maxItems))return false;if(prop.uniqueItems===true){const len=value.length;for(let i=0;i<len;i++){for(let j=i+1;j<len;j++){if((0,_utils.deepEqual)(value[i],value[j]))return false}}}return true}function evaluateCondition(ifSchema,data){if((0,_utils.isPlainObj)(ifSchema.properties)){const propsOk=Object.keys(ifSchema.properties).every(key=>{const propDef=ifSchema.properties?.[key];if(typeof propDef==="boolean")return true;const prop=propDef;const value=data[key];if(value===undefined)return true;if((0,_utils.hasOwn)(prop,"const")){if(!(0,_utils.deepEqual)(value,prop.const))return false}if((0,_utils.hasOwn)(prop,"enum")){if(!prop.enum?.some(v=>(0,_utils.deepEqual)(v,value)))return false}if((0,_utils.hasOwn)(prop,"type")&&value!==undefined){if(!matchesType(value,prop.type))return false}if(typeof value==="number"){if(!evaluateNumericConstraints(value,prop))return false}if(typeof value==="string"){if(!evaluateStringConstraints(value,prop))return false}if(Array.isArray(value)){if(!evaluateArrayConstraints(value,prop))return false}if(prop.format!==undefined&&typeof value==="string"){const formatResult=(0,_formatvalidator.validateFormat)(value,prop.format);if(formatResult===false)return false}if((0,_utils.isPlainObj)(prop.properties)||Array.isArray(prop.required)){if((0,_utils.isPlainObj)(value)){if(!evaluateCondition(prop,value)){return false}}}return true});if(!propsOk)return false}if(Array.isArray(ifSchema.required)){const allRequired=ifSchema.required.every(key=>(0,_utils.hasOwn)(data,key));if(!allRequired)return false}if(Array.isArray(ifSchema.allOf)){const allMatch=ifSchema.allOf.every(entry=>{if(typeof entry==="boolean")return entry;return evaluateCondition(entry,data)});if(!allMatch)return false}if(Array.isArray(ifSchema.anyOf)){const anyMatch=ifSchema.anyOf.some(entry=>{if(typeof entry==="boolean")return entry;return evaluateCondition(entry,data)});if(!anyMatch)return false}if(Array.isArray(ifSchema.oneOf)){let matchCount=0;for(const entry of ifSchema.oneOf){const matches=typeof entry==="boolean"?entry:evaluateCondition(entry,data);if(matches)matchCount++;if(matchCount>1)break}if(matchCount!==1)return false}if((0,_utils.hasOwn)(ifSchema,"not")&&(0,_utils.isPlainObj)(ifSchema.not)&&typeof ifSchema.not!=="boolean"){const notResult=evaluateCondition(ifSchema.not,data);if(notResult)return false}return true}const DISCRIMINANT_INDICATORS=["const","enum","minimum","maximum","exclusiveMinimum","exclusiveMaximum","pattern","minLength","maxLength","multipleOf","minItems","maxItems","format"];function extractDiscriminants(ifSchema,data,out){if(!(0,_utils.isPlainObj)(ifSchema.properties))return;const props=ifSchema.properties;for(const key of Object.keys(props)){const propDef=props[key];if(typeof propDef==="boolean")continue;const prop=propDef;const hasIndicator=DISCRIMINANT_INDICATORS.some(indicator=>(0,_utils.hasOwn)(prop,indicator));if(hasIndicator&&(0,_utils.hasOwn)(data,key)){out[key]=data[key]}}}function mergeBranchInto(resolved,branchDef,engine){if(typeof branchDef==="boolean")return;const branchSchema=branchDef;if(Array.isArray(branchSchema.required)){resolved.required=(0,_utils.unionStrings)(resolved.required??[],branchSchema.required)}if((0,_utils.isPlainObj)(branchSchema.properties)){const branchProps=branchSchema.properties;const mergedProps={...resolved.properties??{}};for(const key of Object.keys(branchProps)){const branchProp=branchProps[key];if(branchProp===undefined)continue;const existing=resolved.properties?.[key];if(existing!==undefined&&typeof existing!=="boolean"&&typeof branchProp!=="boolean"){const merged=engine.merge(existing,branchProp);mergedProps[key]=merged??branchProp}else{mergedProps[key]=branchProp}}resolved.properties=mergedProps}if((0,_utils.isPlainObj)(branchSchema.dependencies)){const resolvedDeps=resolved.dependencies??{};const branchDeps=branchSchema.dependencies;const acc={...resolvedDeps};for(const depKey of Object.keys(branchDeps)){const branchVal=branchDeps[depKey];if(branchVal===undefined)continue;const existingVal=acc[depKey];if(existingVal===undefined){acc[depKey]=branchVal}else if(Array.isArray(existingVal)&&Array.isArray(branchVal)){acc[depKey]=(0,_utils.unionStrings)(existingVal,branchVal)}else if((0,_utils.isPlainObj)(existingVal)&&(0,_utils.isPlainObj)(branchVal)){const merged=engine.merge(existingVal,branchVal);acc[depKey]=merged??branchVal}else{acc[depKey]=branchVal}}resolved.dependencies=acc}for(const key of Object.keys(branchSchema)){if(SPECIAL_MERGE_KEYS.has(key))return;const branchVal=branchSchema[key];const resolvedVal=resolved[key];if(resolvedVal===undefined){resolved[key]=branchVal;return}if((0,_utils.deepEqual)(resolvedVal,branchVal))return;if(SUB_SCHEMA_KEYS.has(key)){const merged=engine.merge(resolvedVal,branchVal);if(merged!==null){resolved[key]=merged}else{resolved[key]=branchVal}return}if(MIN_KEYS.has(key)){if(typeof resolvedVal==="number"&&typeof branchVal==="number"){resolved[key]=Math.max(resolvedVal,branchVal)}else{resolved[key]=branchVal}return}if(MAX_KEYS.has(key)){if(typeof resolvedVal==="number"&&typeof branchVal==="number"){resolved[key]=Math.min(resolvedVal,branchVal)}else{resolved[key]=branchVal}return}if(key==="uniqueItems"){resolved[key]=resolvedVal===true||branchVal===true;return}if(key==="pattern"||key==="format"){resolved[key]=branchVal;return}const base={[key]:resolvedVal};const branch={[key]:branchVal};const merged=engine.merge(base,branch);if(merged&&typeof merged!=="boolean"&&(0,_utils.hasOwn)(merged,key)){resolved[key]=merged[key]}else{resolved[key]=branchVal}}}function resolveConditions(schema,data,engine){let branch=null;const discriminant={};const hasTopLevelIf=schema.if!==undefined;const hasAllOfConditions=Array.isArray(schema.allOf)&&schema.allOf.some(e=>typeof e!=="boolean"&&(0,_utils.hasOwn)(e,"if"));if(!hasTopLevelIf&&!hasAllOfConditions){const resolved=resolveNestedProperties(schema,data,engine,discriminant);return{resolved,branch,discriminant}}let resolved={...schema};if(hasAllOfConditions){resolved=resolveAllOfConditions(resolved,data,engine,discriminant)}if(resolved.if!==undefined){const ifSchema=resolved.if;const matches=evaluateCondition(ifSchema,data);extractDiscriminants(ifSchema,data,discriminant);const applicableBranch=matches?resolved.then:resolved.else;branch=matches?"then":"else";if(applicableBranch){mergeBranchInto(resolved,applicableBranch,engine)}delete resolved.if;delete resolved.then;delete resolved.else}resolved=resolveNestedProperties(resolved,data,engine,discriminant);return{resolved,branch,discriminant}}function resolveAllOfConditions(resolved,data,engine,discriminant){if(!Array.isArray(resolved.allOf))return resolved;const remainingAllOf=[];for(const entry of resolved.allOf){if(typeof entry==="boolean"){remainingAllOf.push(entry);continue}const subSchema=entry;if(subSchema.if===undefined){remainingAllOf.push(entry);continue}const ifSchema=subSchema.if;const matches=evaluateCondition(ifSchema,data);extractDiscriminants(ifSchema,data,discriminant);const applicableBranch=matches?subSchema.then:subSchema.else;if(applicableBranch){mergeBranchInto(resolved,applicableBranch,engine)}const remaining=(0,_utils.omitKeys)(subSchema,["if","then","else"]);if(Object.keys(remaining).length>0){remainingAllOf.push(remaining)}}resolved={...resolved};if(remainingAllOf.length===0){delete resolved.allOf}else{resolved.allOf=remainingAllOf}return resolved}function resolveNestedProperties(resolved,data,engine,discriminant){if(!(0,_utils.isPlainObj)(resolved.properties))return resolved;const props=resolved.properties;const propKeys=Object.keys(props);let changed=false;const resolvedProps={};for(const key of propKeys){const propDef=props[key];if(propDef===undefined)continue;if(typeof propDef==="boolean"){resolvedProps[key]=propDef;continue}const propSchema=propDef;const hasConditions=propSchema.if!==undefined||Array.isArray(propSchema.allOf)&&propSchema.allOf.some(e=>typeof e!=="boolean"&&(0,_utils.hasOwn)(e,"if"));if(!hasConditions){resolvedProps[key]=propDef;continue}const nestedData=(0,_utils.isPlainObj)(data[key])?data[key]:{};const nested=resolveConditions(propSchema,nestedData,engine);for(const dk of Object.keys(nested.discriminant)){discriminant[`${key}.${dk}`]=nested.discriminant[dk]}resolvedProps[key]=nested.resolved;changed=true}return changed?{...resolved,properties:resolvedProps}:resolved}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});Object.defineProperty(exports,"resolveConditions",{enumerable:true,get:function(){return resolveConditions}});const _runtimevalidatorts=require("./runtime-validator.js");const _utilsts=require("./utils.js");const SPECIAL_MERGE_KEYS=new Set(["required","properties","dependencies"]);const SUB_SCHEMA_KEYS=new Set(["additionalProperties","items","contains","propertyNames","not"]);const MIN_KEYS=new Set(["minimum","exclusiveMinimum","minLength","minItems","minProperties"]);const MAX_KEYS=new Set(["maximum","exclusiveMaximum","maxLength","maxItems","maxProperties"]);function evaluateCondition(ifSchema,data){return(0,_runtimevalidatorts.isDataValidForSchema)(ifSchema,data)}const DISCRIMINANT_INDICATORS=["const","enum","minimum","maximum","exclusiveMinimum","exclusiveMaximum","pattern","minLength","maxLength","multipleOf","minItems","maxItems","format"];function extractDiscriminants(ifSchema,data,out){if(!(0,_utilsts.isPlainObj)(ifSchema.properties))return;const props=ifSchema.properties;for(const key of Object.keys(props)){const propDef=props[key];if(typeof propDef==="boolean")continue;const prop=propDef;const hasIndicator=DISCRIMINANT_INDICATORS.some(indicator=>(0,_utilsts.hasOwn)(prop,indicator));if(hasIndicator&&(0,_utilsts.hasOwn)(data,key)){out[key]=data[key]}}}function mergeBranchInto(resolved,branchDef,engine){if(typeof branchDef==="boolean")return;const branchSchema=branchDef;if(Array.isArray(branchSchema.required)){resolved.required=(0,_utilsts.unionStrings)(resolved.required??[],branchSchema.required)}if((0,_utilsts.isPlainObj)(branchSchema.properties)){const branchProps=branchSchema.properties;const mergedProps={...resolved.properties??{}};for(const key of Object.keys(branchProps)){const branchProp=branchProps[key];if(branchProp===undefined)continue;const existing=resolved.properties?.[key];if(existing!==undefined&&typeof existing!=="boolean"&&typeof branchProp!=="boolean"){const merged=engine.merge(existing,branchProp);mergedProps[key]=merged??branchProp}else{mergedProps[key]=branchProp}}resolved.properties=mergedProps}if((0,_utilsts.isPlainObj)(branchSchema.dependencies)){const resolvedDeps=resolved.dependencies??{};const branchDeps=branchSchema.dependencies;const acc={...resolvedDeps};for(const depKey of Object.keys(branchDeps)){const branchVal=branchDeps[depKey];if(branchVal===undefined)continue;const existingVal=acc[depKey];if(existingVal===undefined){acc[depKey]=branchVal}else if(Array.isArray(existingVal)&&Array.isArray(branchVal)){acc[depKey]=(0,_utilsts.unionStrings)(existingVal,branchVal)}else if((0,_utilsts.isPlainObj)(existingVal)&&(0,_utilsts.isPlainObj)(branchVal)){const merged=engine.merge(existingVal,branchVal);acc[depKey]=merged??branchVal}else{acc[depKey]=branchVal}}resolved.dependencies=acc}for(const key of Object.keys(branchSchema)){if(SPECIAL_MERGE_KEYS.has(key))continue;const branchVal=branchSchema[key];const resolvedVal=resolved[key];if(resolvedVal===undefined){resolved[key]=branchVal;continue}if((0,_utilsts.deepEqual)(resolvedVal,branchVal))continue;if(SUB_SCHEMA_KEYS.has(key)){const merged=engine.merge(resolvedVal,branchVal);if(merged!==null){resolved[key]=merged}else{resolved[key]=branchVal}continue}if(MIN_KEYS.has(key)){if(typeof resolvedVal==="number"&&typeof branchVal==="number"){resolved[key]=Math.max(resolvedVal,branchVal)}else{resolved[key]=branchVal}continue}if(MAX_KEYS.has(key)){if(typeof resolvedVal==="number"&&typeof branchVal==="number"){resolved[key]=Math.min(resolvedVal,branchVal)}else{resolved[key]=branchVal}continue}if(key==="uniqueItems"){resolved[key]=resolvedVal===true||branchVal===true;continue}if(key==="pattern"||key==="format"){resolved[key]=branchVal;continue}if(key==="constraints"){const merged=(0,_utilsts.mergeConstraints)(resolvedVal,branchVal);if(merged!==undefined){resolved[key]=merged}continue}const base={[key]:resolvedVal};const branch={[key]:branchVal};const merged=engine.merge(base,branch);if(merged&&typeof merged!=="boolean"&&(0,_utilsts.hasOwn)(merged,key)){resolved[key]=merged[key]}else{resolved[key]=branchVal}}}function resolveConditions(schema,data,engine){let branch=null;const discriminant={};const hasTopLevelIf=schema.if!==undefined;const hasAllOfConditions=Array.isArray(schema.allOf)&&schema.allOf.some(e=>typeof e!=="boolean"&&(0,_utilsts.hasOwn)(e,"if"));if(!hasTopLevelIf&&!hasAllOfConditions){const resolved=resolveNestedProperties(schema,data,engine,discriminant);return{resolved,branch,discriminant}}let resolved={...schema};if(hasAllOfConditions){resolved=resolveAllOfConditions(resolved,data,engine,discriminant)}if(resolved.if!==undefined){const ifSchema=resolved.if;const matches=evaluateCondition(ifSchema,data);extractDiscriminants(ifSchema,data,discriminant);const applicableBranch=matches?resolved.then:resolved.else;branch=matches?"then":"else";if(applicableBranch){mergeBranchInto(resolved,applicableBranch,engine)}delete resolved.if;delete resolved.then;delete resolved.else}resolved=resolveNestedProperties(resolved,data,engine,discriminant);return{resolved,branch,discriminant}}function resolveAllOfConditions(resolved,data,engine,discriminant){if(!Array.isArray(resolved.allOf))return resolved;const remainingAllOf=[];for(const entry of resolved.allOf){if(typeof entry==="boolean"){remainingAllOf.push(entry);continue}const subSchema=entry;if(subSchema.if===undefined){remainingAllOf.push(entry);continue}const ifSchema=subSchema.if;const matches=evaluateCondition(ifSchema,data);extractDiscriminants(ifSchema,data,discriminant);const applicableBranch=matches?subSchema.then:subSchema.else;if(applicableBranch){mergeBranchInto(resolved,applicableBranch,engine)}const remaining=(0,_utilsts.omitKeys)(subSchema,["if","then","else"]);if(Object.keys(remaining).length>0){remainingAllOf.push(remaining)}}resolved={...resolved};if(remainingAllOf.length===0){delete resolved.allOf}else{resolved.allOf=remainingAllOf}return resolved}function resolveNestedProperties(resolved,data,engine,discriminant){if(!(0,_utilsts.isPlainObj)(resolved.properties))return resolved;const props=resolved.properties;const propKeys=Object.keys(props);let changed=false;const resolvedProps={};for(const key of propKeys){const propDef=props[key];if(propDef===undefined)continue;if(typeof propDef==="boolean"){resolvedProps[key]=propDef;continue}const propSchema=propDef;const hasConditions=propSchema.if!==undefined||Array.isArray(propSchema.allOf)&&propSchema.allOf.some(e=>typeof e!=="boolean"&&(0,_utilsts.hasOwn)(e,"if"));if(!hasConditions){resolvedProps[key]=propDef;continue}const nestedData=(0,_utilsts.isPlainObj)(data[key])?data[key]:{};const nested=resolveConditions(propSchema,nestedData,engine);for(const dk of Object.keys(nested.discriminant)){discriminant[`${key}.${dk}`]=nested.discriminant[dk]}resolvedProps[key]=nested.resolved;changed=true}return changed?{...resolved,properties:resolvedProps}:resolved}
|
|
2
2
|
//# sourceMappingURL=condition-resolver.js.map
|