json-schema-compatibility-checker 1.0.7 → 1.0.9
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 +72 -1818
- package/dist/cjs/merge-engine.d.ts +91 -0
- package/dist/cjs/merge-engine.js +1 -1
- package/dist/cjs/merge-engine.js.map +1 -1
- package/dist/esm/merge-engine.d.ts +91 -0
- package/dist/esm/merge-engine.js +1 -1
- package/dist/esm/merge-engine.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,41 +11,9 @@
|
|
|
11
11
|
- [Installation](#installation)
|
|
12
12
|
- [Démarrage rapide](#démarrage-rapide)
|
|
13
13
|
- [API Reference](#api-reference)
|
|
14
|
-
|
|
15
|
-
- [`check(sub, sup)`](#checksub-sup)
|
|
16
|
-
- [`isEqual(a, b)`](#isequala-b)
|
|
17
|
-
- [`intersect(a, b)`](#intersecta-b)
|
|
18
|
-
- [`resolveConditions(schema, data)`](#resolveconditionsschema-data)
|
|
19
|
-
- [`check(sub, sup, options)`](#checksub-sup-options)
|
|
20
|
-
- [`normalize(schema)`](#normalizeschema)
|
|
21
|
-
- [`formatResult(label, result)`](#formatresultlabel-result)
|
|
22
|
-
- [Guide des fonctionnalités](#guide-des-fonctionnalités)
|
|
23
|
-
- [1. Compatibilité de types](#1-compatibilité-de-types)
|
|
24
|
-
- [2. Champs requis (`required`)](#2-champs-requis-required)
|
|
25
|
-
- [3. Contraintes numériques](#3-contraintes-numériques)
|
|
26
|
-
- [4. Contraintes de chaînes](#4-contraintes-de-chaînes)
|
|
27
|
-
- [5. `enum` et `const`](#5-enum-et-const)
|
|
28
|
-
- [6. Contraintes de tableaux](#6-contraintes-de-tableaux)
|
|
29
|
-
- [7. `additionalProperties`](#7-additionalproperties)
|
|
30
|
-
- [8. Objets imbriqués](#8-objets-imbriqués)
|
|
31
|
-
- [9. `anyOf` / `oneOf`](#9-anyof--oneof)
|
|
32
|
-
- [10. Négation (`not`)](#10-négation-not)
|
|
33
|
-
- [11. Formats (`format`)](#11-formats-format)
|
|
34
|
-
- [12. Patterns regex (`pattern`)](#12-patterns-regex-pattern)
|
|
35
|
-
- [13. Conditions `if` / `then` / `else`](#13-conditions-if--then--else)
|
|
36
|
-
- [14. `allOf` avec conditions](#14-allof-avec-conditions)
|
|
37
|
-
- [Fonctions utilitaires](#fonctions-utilitaires)
|
|
38
|
-
- [`isPatternSubset(sub, sup)`](#ispatternsubsetsub-sup)
|
|
39
|
-
- [`arePatternsEquivalent(a, b)`](#arepatternsEquivalenta-b)
|
|
40
|
-
- [`isTrivialPattern(pattern)`](#istrivialpatternpattern)
|
|
41
|
-
- [Cas d'usage concrets](#cas-dusage-concrets)
|
|
42
|
-
- [Connexion de nœuds dans un orchestrateur](#connexion-de-nœuds-dans-un-orchestrateur)
|
|
43
|
-
- [Validation de réponse API](#validation-de-réponse-api)
|
|
44
|
-
- [Union discriminée](#union-discriminée)
|
|
45
|
-
- [Formulaire conditionnel](#formulaire-conditionnel)
|
|
46
|
-
- [Types exportés](#types-exportés)
|
|
14
|
+
- [Documentation complète](#-documentation-complète)
|
|
47
15
|
- [Limitations connues](#limitations-connues)
|
|
48
|
-
- [
|
|
16
|
+
- [Licence](#licence)
|
|
49
17
|
|
|
50
18
|
---
|
|
51
19
|
|
|
@@ -67,7 +35,8 @@ Cette librairie répond à cette question en vérifiant si un schema est un **so
|
|
|
67
35
|
|
|
68
36
|
- ✅ Vérifie si un schema est un sous-ensemble d'un autre (`sub ⊆ sup`)
|
|
69
37
|
- ✅ Produit un diagnostic détaillé avec les différences structurelles
|
|
70
|
-
- ✅ Calcule l'intersection de deux schemas
|
|
38
|
+
- ✅ Calcule l'intersection de deux schemas (`allOf` merge)
|
|
39
|
+
- ✅ Accumule des schemas séquentiellement via deep spread (`overlay`)
|
|
71
40
|
- ✅ Résout les conditions `if/then/else` avec des données discriminantes
|
|
72
41
|
- ✅ Gère `anyOf`, `oneOf`, `not`, `format`, `pattern`, `dependencies`, etc.
|
|
73
42
|
- ✅ Compare des patterns regex par échantillonnage
|
|
@@ -148,1842 +117,127 @@ console.log(checker.isSubset(loose, strict)); // false ❌
|
|
|
148
117
|
|
|
149
118
|
## API Reference
|
|
150
119
|
|
|
151
|
-
|
|
120
|
+
### `JsonSchemaCompatibilityChecker`
|
|
152
121
|
|
|
153
|
-
|
|
154
|
-
import { JsonSchemaCompatibilityChecker } from "json-schema-compatibility-checker";
|
|
155
|
-
|
|
156
|
-
const checker = new JsonSchemaCompatibilityChecker();
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
### `isSubset(sub, sup)`
|
|
122
|
+
Toutes les méthodes de vérification de compatibilité sont exposées par la classe `JsonSchemaCompatibilityChecker`.
|
|
162
123
|
|
|
163
124
|
```ts
|
|
164
|
-
|
|
125
|
+
const checker = new JsonSchemaCompatibilityChecker();
|
|
165
126
|
```
|
|
166
127
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
checker.isSubset({ type: "number" }, { type: "integer" });
|
|
178
|
-
// → false
|
|
128
|
+
| Méthode | Description | Retour |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `isSubset(sub, sup)` | Vérifie si `sub ⊆ sup` | `boolean` |
|
|
131
|
+
| `check(sub, sup)` | Vérifie avec diagnostic détaillé | `SubsetResult` |
|
|
132
|
+
| `check(sub, sup, options)` | Vérifie avec résolution des conditions `if/then/else` | `ResolvedSubsetResult` |
|
|
133
|
+
| `isEqual(a, b)` | Égalité structurelle après normalisation | `boolean` |
|
|
134
|
+
| `intersect(a, b)` | Intersection de deux schemas | `JSONSchema7Definition \| null` |
|
|
135
|
+
| `resolveConditions(schema, data)` | Résout les `if/then/else` avec des données | `ResolvedConditionResult` |
|
|
136
|
+
| `normalize(schema)` | Normalise un schema (infère types, résout double négation) | `JSONSchema7Definition` |
|
|
137
|
+
| `formatResult(label, result)` | Formate un résultat pour le debug | `string` |
|
|
179
138
|
|
|
180
|
-
|
|
181
|
-
checker.isSubset({ type: "string" }, { type: "number" });
|
|
182
|
-
// → false
|
|
183
|
-
```
|
|
139
|
+
### `MergeEngine`
|
|
184
140
|
|
|
185
|
-
|
|
141
|
+
Opérations bas-niveau sur les schemas : intersection (`allOf` merge) et overlay (deep spread séquentiel).
|
|
186
142
|
|
|
187
143
|
```ts
|
|
188
|
-
|
|
189
|
-
checker.isSubset(false, true); // → true
|
|
190
|
-
checker.isSubset(false, { type: "string" }); // → true
|
|
191
|
-
|
|
192
|
-
// true (toutes les valeurs) n'est sous-ensemble de rien de spécifique
|
|
193
|
-
checker.isSubset(true, { type: "string" }); // → false
|
|
194
|
-
|
|
195
|
-
// Tout schema est sous-ensemble de true
|
|
196
|
-
checker.isSubset({ type: "string" }, true); // → true
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
### `check(sub, sup)`
|
|
144
|
+
import { MergeEngine } from "json-schema-compatibility-checker";
|
|
202
145
|
|
|
203
|
-
|
|
204
|
-
check(sub: JSONSchema7Definition, sup: JSONSchema7Definition): SubsetResult
|
|
205
|
-
check(sub: JSONSchema7Definition, sup: JSONSchema7Definition, options: CheckConditionsOptions): ResolvedSubsetResult
|
|
146
|
+
const engine = new MergeEngine();
|
|
206
147
|
```
|
|
207
148
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
expected: string; // Type/valeur attendu(e) par le schema cible (sup)
|
|
216
|
-
received: string; // Type/valeur reçu(e) depuis le schema source (sub)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
interface SubsetResult {
|
|
220
|
-
isSubset: boolean;
|
|
221
|
-
merged: JSONSchema7Definition | null; // Résultat de l'intersection
|
|
222
|
-
errors: SchemaError[]; // Erreurs sémantiques
|
|
223
|
-
}
|
|
224
|
-
```
|
|
149
|
+
| Méthode | Description | Retour |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `merge(a, b)` | Intersection `allOf([a, b])` — retourne `null` si incompatible | `JSONSchema7Definition \| null` |
|
|
152
|
+
| `mergeOrThrow(a, b)` | Comme `merge`, mais lève une exception si incompatible | `JSONSchema7Definition` |
|
|
153
|
+
| `overlay(base, override)` | Deep spread séquentiel — last writer wins par propriété | `JSONSchema7Definition` |
|
|
154
|
+
| `compare(a, b)` | Comparaison structurelle (0 = identique) | `number` |
|
|
155
|
+
| `isEqual(a, b)` | Égalité structurelle | `boolean` |
|
|
225
156
|
|
|
226
|
-
|
|
157
|
+
**Exemple rapide — `check` avec diagnostic :**
|
|
227
158
|
|
|
228
159
|
```ts
|
|
229
160
|
const result = checker.check(
|
|
230
|
-
{ type: "string",
|
|
231
|
-
{ type: "string" }
|
|
161
|
+
{ type: "object", properties: { name: { type: "string" } }, required: ["name"] },
|
|
162
|
+
{ type: "object", properties: { name: { type: "string" }, age: { type: "number" } }, required: ["name", "age"] }
|
|
232
163
|
);
|
|
233
164
|
|
|
234
|
-
console.log(result.isSubset); // true
|
|
235
|
-
console.log(result.errors); // [] (aucune erreur)
|
|
236
|
-
console.log(result.merged); // { type: "string", minLength: 5 }
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
#### Exemple — Check incompatible avec diagnostic
|
|
240
|
-
|
|
241
|
-
```ts
|
|
242
|
-
const sub = {
|
|
243
|
-
type: "object",
|
|
244
|
-
properties: { name: { type: "string" } },
|
|
245
|
-
required: ["name"],
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const sup = {
|
|
249
|
-
type: "object",
|
|
250
|
-
properties: {
|
|
251
|
-
name: { type: "string" },
|
|
252
|
-
age: { type: "number" },
|
|
253
|
-
},
|
|
254
|
-
required: ["name", "age"],
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const result = checker.check(sub, sup);
|
|
258
|
-
console.log(result.errors);
|
|
259
|
-
// [{ key: "age", expected: "number", received: "undefined" }]
|
|
260
|
-
|
|
261
165
|
console.log(result.isSubset); // false
|
|
262
166
|
console.log(result.errors);
|
|
263
|
-
// [
|
|
264
|
-
// { key: "age", expected: "number", received: "undefined" }
|
|
265
|
-
// ]
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
#### Exemple — Types incompatibles
|
|
269
|
-
|
|
270
|
-
```ts
|
|
271
|
-
const result = checker.check({ type: "string" }, { type: "number" });
|
|
272
|
-
|
|
273
|
-
console.log(result.isSubset); // false
|
|
274
|
-
console.log(result.merged); // null (intersection impossible)
|
|
275
|
-
console.log(result.errors); // [{ key: "$root", expected: "number", received: "string" }]
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
---
|
|
279
|
-
|
|
280
|
-
### `isEqual(a, b)`
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
isEqual(a: JSONSchema7Definition, b: JSONSchema7Definition): boolean
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
Vérifie l'**égalité structurelle** entre deux schemas après normalisation.
|
|
287
|
-
|
|
288
|
-
```ts
|
|
289
|
-
checker.isEqual(
|
|
290
|
-
{ type: "string", minLength: 1 },
|
|
291
|
-
{ type: "string", minLength: 1 }
|
|
292
|
-
);
|
|
293
|
-
// → true
|
|
294
|
-
|
|
295
|
-
checker.isEqual(
|
|
296
|
-
{ type: "string" },
|
|
297
|
-
{ type: "number" }
|
|
298
|
-
);
|
|
299
|
-
// → false
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
### `intersect(a, b)`
|
|
305
|
-
|
|
306
|
-
```ts
|
|
307
|
-
intersect(
|
|
308
|
-
a: JSONSchema7Definition,
|
|
309
|
-
b: JSONSchema7Definition
|
|
310
|
-
): JSONSchema7Definition | null
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
Calcule l'**intersection** de deux schemas (merge `allOf`). Retourne `null` si les schemas sont incompatibles.
|
|
314
|
-
|
|
315
|
-
#### Exemple — Intersection de contraintes numériques
|
|
316
|
-
|
|
317
|
-
```ts
|
|
318
|
-
const result = checker.intersect(
|
|
319
|
-
{ type: "number", minimum: 5, maximum: 10 },
|
|
320
|
-
{ type: "number", minimum: 0, maximum: 100 }
|
|
321
|
-
);
|
|
322
|
-
// → { type: "number", minimum: 5, maximum: 10 }
|
|
323
|
-
// L'intersection conserve les contraintes les plus restrictives
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
#### Exemple — Intersection de propriétés d'objets
|
|
327
|
-
|
|
328
|
-
```ts
|
|
329
|
-
const result = checker.intersect(
|
|
330
|
-
{
|
|
331
|
-
type: "object",
|
|
332
|
-
properties: { a: { type: "string" } },
|
|
333
|
-
required: ["a"],
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
type: "object",
|
|
337
|
-
properties: { b: { type: "number" } },
|
|
338
|
-
required: ["b"],
|
|
339
|
-
}
|
|
340
|
-
);
|
|
341
|
-
// → {
|
|
342
|
-
// type: "object",
|
|
343
|
-
// properties: { a: { type: "string" }, b: { type: "number" } },
|
|
344
|
-
// required: ["a", "b"]
|
|
345
|
-
// }
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
#### Exemple — Intersection d'enums
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
const result = checker.intersect(
|
|
352
|
-
{ type: "string", enum: ["a", "b", "c"] },
|
|
353
|
-
{ type: "string", enum: ["b", "c", "d"] }
|
|
354
|
-
);
|
|
355
|
-
// → { type: "string", enum: ["b", "c"] }
|
|
356
|
-
// Seules les valeurs communes sont conservées
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
#### Exemple — Types incompatibles
|
|
360
|
-
|
|
361
|
-
```ts
|
|
362
|
-
checker.intersect({ type: "string" }, { type: "number" });
|
|
363
|
-
// → null (aucune valeur ne peut être à la fois string ET number)
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
---
|
|
367
|
-
|
|
368
|
-
### `resolveConditions(schema, data)`
|
|
369
|
-
|
|
370
|
-
```ts
|
|
371
|
-
resolveConditions(
|
|
372
|
-
schema: JSONSchema7,
|
|
373
|
-
data: Record<string, unknown>
|
|
374
|
-
): ResolvedConditionResult
|
|
167
|
+
// [{ key: "age", expected: "number", received: "undefined" }]
|
|
375
168
|
```
|
|
376
169
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
```ts
|
|
380
|
-
interface ResolvedConditionResult {
|
|
381
|
-
resolved: JSONSchema7; // Schema avec if/then/else résolus
|
|
382
|
-
branch: "then" | "else" | null; // Branche appliquée
|
|
383
|
-
discriminant: Record<string, unknown>; // Discriminant utilisé
|
|
384
|
-
}
|
|
385
|
-
```
|
|
170
|
+
**Exemple rapide — résolution de conditions :**
|
|
386
171
|
|
|
387
172
|
```ts
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
properties: {
|
|
391
|
-
accountType: { type: "string", enum: ["personal", "business"] },
|
|
392
|
-
email: { type: "string", format: "email" },
|
|
393
|
-
companyName: { type: "string" },
|
|
394
|
-
firstName: { type: "string" },
|
|
395
|
-
},
|
|
396
|
-
required: ["accountType", "email"],
|
|
397
|
-
if: {
|
|
398
|
-
properties: { accountType: { const: "business" } },
|
|
399
|
-
required: ["accountType"],
|
|
400
|
-
},
|
|
401
|
-
then: {
|
|
402
|
-
required: ["companyName"],
|
|
403
|
-
},
|
|
404
|
-
else: {
|
|
405
|
-
required: ["firstName"],
|
|
406
|
-
},
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
// Résoudre pour un compte business
|
|
410
|
-
const business = checker.resolveConditions(formSchema, {
|
|
411
|
-
accountType: "business",
|
|
412
|
-
});
|
|
413
|
-
console.log(business.branch); // "then"
|
|
414
|
-
console.log(business.resolved.required);
|
|
415
|
-
// → ["accountType", "email", "companyName"]
|
|
416
|
-
|
|
417
|
-
// Résoudre pour un compte personnel
|
|
418
|
-
const personal = checker.resolveConditions(formSchema, {
|
|
419
|
-
accountType: "personal",
|
|
173
|
+
const result = checker.check(sub, conditionalSup, {
|
|
174
|
+
subData: { kind: "text" },
|
|
420
175
|
});
|
|
421
|
-
console.log(personal.branch); // "else"
|
|
422
|
-
console.log(personal.resolved.required);
|
|
423
|
-
// → ["accountType", "email", "firstName"]
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
---
|
|
427
|
-
|
|
428
|
-
### `check(sub, sup, options)`
|
|
429
|
-
|
|
430
|
-
```ts
|
|
431
|
-
check(
|
|
432
|
-
sub: JSONSchema7Definition,
|
|
433
|
-
sup: JSONSchema7Definition,
|
|
434
|
-
options: CheckConditionsOptions
|
|
435
|
-
): ResolvedSubsetResult
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
Résout les conditions `if/then/else` des deux schemas **puis** vérifie `sub ⊆ sup`. Utile quand le superset contient des `if/then/else` et que vous connaissez les valeurs discriminantes. Effectue aussi un **narrowing** du sub par rapport aux contraintes `enum`/`const` du sup en utilisant les données runtime.
|
|
439
|
-
|
|
440
|
-
```ts
|
|
441
|
-
interface CheckConditionsOptions {
|
|
442
|
-
/** Runtime data for the sub schema — used for condition resolution and enum narrowing */
|
|
443
|
-
subData: unknown;
|
|
444
|
-
/** Runtime data for the sup schema (defaults to subData) — used for condition resolution and enum narrowing */
|
|
445
|
-
supData?: unknown;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
interface ResolvedSubsetResult extends SubsetResult {
|
|
449
|
-
resolvedSub: ResolvedConditionResult;
|
|
450
|
-
resolvedSup: ResolvedConditionResult;
|
|
451
|
-
}
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
```ts
|
|
455
|
-
const conditionalSup = {
|
|
456
|
-
type: "object",
|
|
457
|
-
properties: {
|
|
458
|
-
kind: { type: "string" },
|
|
459
|
-
value: {},
|
|
460
|
-
},
|
|
461
|
-
required: ["kind", "value"],
|
|
462
|
-
if: {
|
|
463
|
-
properties: { kind: { const: "text" } },
|
|
464
|
-
required: ["kind"],
|
|
465
|
-
},
|
|
466
|
-
then: {
|
|
467
|
-
properties: { value: { type: "string" } },
|
|
468
|
-
},
|
|
469
|
-
else: {
|
|
470
|
-
properties: { value: { type: "number" } },
|
|
471
|
-
},
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
const sub = {
|
|
475
|
-
type: "object",
|
|
476
|
-
properties: {
|
|
477
|
-
kind: { const: "text" },
|
|
478
|
-
value: { type: "string", minLength: 1 },
|
|
479
|
-
},
|
|
480
|
-
required: ["kind", "value"],
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
// Sans résolution : false (le if/then/else brut ne matche pas)
|
|
484
|
-
console.log(checker.isSubset(sub, conditionalSup)); // false
|
|
485
|
-
|
|
486
|
-
// Avec résolution via options : true !
|
|
487
|
-
const result = checker.check(sub, conditionalSup, { subData: { kind: "text" } });
|
|
488
176
|
console.log(result.isSubset); // true ✅
|
|
489
177
|
console.log(result.resolvedSup.branch); // "then"
|
|
490
|
-
|
|
491
|
-
// Avec des données différentes pour sub et sup
|
|
492
|
-
const result2 = checker.check(sub, conditionalSup, {
|
|
493
|
-
subData: { kind: "text" },
|
|
494
|
-
supData: { kind: "text" },
|
|
495
|
-
});
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
---
|
|
499
|
-
|
|
500
|
-
### `normalize(schema)`
|
|
501
|
-
|
|
502
|
-
```ts
|
|
503
|
-
normalize(def: JSONSchema7Definition): JSONSchema7Definition
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
Normalise un schema : infère `type` depuis `const`/`enum`, résout la double négation `not(not(X)) → X`, et normalise récursivement tous les sous-schemas.
|
|
507
|
-
|
|
508
|
-
```ts
|
|
509
|
-
// Infère le type depuis const
|
|
510
|
-
checker.normalize({ const: "hello" });
|
|
511
|
-
// → { const: "hello", type: "string" }
|
|
512
|
-
|
|
513
|
-
// Infère le type depuis enum
|
|
514
|
-
checker.normalize({ enum: [1, 2, 3] });
|
|
515
|
-
// → { enum: [1, 2, 3], type: "integer" }
|
|
516
|
-
|
|
517
|
-
// Convertit enum à un seul élément en const
|
|
518
|
-
checker.normalize({ enum: ["only"] });
|
|
519
|
-
// → { const: "only", type: "string" }
|
|
520
|
-
|
|
521
|
-
// Résout la double négation
|
|
522
|
-
checker.normalize({ not: { not: { type: "string" } } });
|
|
523
|
-
// → { type: "string" }
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
---
|
|
527
|
-
|
|
528
|
-
### `formatResult(label, result)`
|
|
529
|
-
|
|
530
|
-
```ts
|
|
531
|
-
formatResult(label: string, result: SubsetResult): string
|
|
532
178
|
```
|
|
533
179
|
|
|
534
|
-
|
|
180
|
+
**Exemple rapide — `overlay` pour accumulation séquentielle :**
|
|
535
181
|
|
|
536
182
|
```ts
|
|
537
|
-
|
|
538
|
-
{ type: "number", minimum: 0, maximum: 100 },
|
|
539
|
-
{ type: "number", minimum: 5, maximum: 10 }
|
|
540
|
-
);
|
|
541
|
-
|
|
542
|
-
console.log(checker.formatResult("range check", result));
|
|
543
|
-
// ❌ range check: false
|
|
544
|
-
// Errors:
|
|
545
|
-
// ✗ $root: expected minimum 5, received minimum 0
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
```ts
|
|
549
|
-
const result2 = checker.check(
|
|
550
|
-
{ type: "string", minLength: 5 },
|
|
551
|
-
{ type: "string" }
|
|
552
|
-
);
|
|
553
|
-
|
|
554
|
-
console.log(checker.formatResult("strict ⊆ loose", result2));
|
|
555
|
-
// ✅ strict ⊆ loose: true
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
Format de sortie :
|
|
559
|
-
- `✅` — le check a réussi (`isSubset: true`)
|
|
560
|
-
- `❌` — le check a échoué (`isSubset: false`), suivi de la liste des erreurs
|
|
561
|
-
- `✗ key: expected X, received Y` — détail de chaque erreur sémantique
|
|
562
|
-
|
|
563
|
-
---
|
|
564
|
-
|
|
565
|
-
## Guide des fonctionnalités
|
|
566
|
-
|
|
567
|
-
Cette section présente les fonctionnalités supportées, du plus simple au plus complexe, avec des exemples illustratifs.
|
|
568
|
-
|
|
569
|
-
---
|
|
183
|
+
import { MergeEngine } from "json-schema-compatibility-checker";
|
|
570
184
|
|
|
571
|
-
|
|
185
|
+
const engine = new MergeEngine();
|
|
572
186
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
```ts
|
|
576
|
-
// integer ⊆ number (tout entier est un nombre)
|
|
577
|
-
checker.isSubset({ type: "integer" }, { type: "number" }); // true
|
|
578
|
-
|
|
579
|
-
// number ⊄ integer (1.5 est un nombre mais pas un entier)
|
|
580
|
-
checker.isSubset({ type: "number" }, { type: "integer" }); // false
|
|
581
|
-
|
|
582
|
-
// Types incompatibles
|
|
583
|
-
checker.isSubset({ type: "string" }, { type: "number" }); // false
|
|
584
|
-
checker.isSubset({ type: "boolean" }, { type: "string" }); // false
|
|
585
|
-
|
|
586
|
-
// Identité
|
|
587
|
-
checker.isSubset({ type: "string" }, { type: "string" }); // true
|
|
588
|
-
|
|
589
|
-
// L'intersection integer ∩ number = integer
|
|
590
|
-
checker.intersect({ type: "integer" }, { type: "number" });
|
|
591
|
-
// → { type: "integer" }
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
---
|
|
595
|
-
|
|
596
|
-
### 2. Champs requis (`required`)
|
|
597
|
-
|
|
598
|
-
Un schema qui exige **plus** de champs est un sous-ensemble d'un schema qui en exige **moins**.
|
|
599
|
-
|
|
600
|
-
```ts
|
|
601
|
-
const strict = {
|
|
187
|
+
// Node1 produit accountId avec enum
|
|
188
|
+
const node1Output = {
|
|
602
189
|
type: "object",
|
|
603
|
-
properties: {
|
|
604
|
-
|
|
605
|
-
age: { type: "number" },
|
|
606
|
-
},
|
|
607
|
-
required: ["name", "age"],
|
|
190
|
+
properties: { accountId: { type: "string", enum: ["a", "b"] } },
|
|
191
|
+
required: ["accountId"],
|
|
608
192
|
};
|
|
609
193
|
|
|
610
|
-
|
|
194
|
+
// Node2 redéfinit accountId en string simple (plus large)
|
|
195
|
+
const node2Output = {
|
|
611
196
|
type: "object",
|
|
612
|
-
properties: {
|
|
613
|
-
|
|
614
|
-
},
|
|
615
|
-
required: ["name"],
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
// Plus de champs requis → plus restrictif → sous-ensemble
|
|
619
|
-
checker.isSubset(strict, loose); // true
|
|
620
|
-
|
|
621
|
-
// Moins de champs requis → plus permissif → PAS sous-ensemble
|
|
622
|
-
checker.isSubset(loose, strict); // false
|
|
623
|
-
|
|
624
|
-
// Le diagnostic montre exactement ce qui manque
|
|
625
|
-
const result = checker.check(loose, strict);
|
|
626
|
-
console.log(result.errors);
|
|
627
|
-
// [{ key: "age", expected: "number", received: "undefined" }]
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
---
|
|
631
|
-
|
|
632
|
-
### 3. Contraintes numériques
|
|
633
|
-
|
|
634
|
-
La librairie gère `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum` et `multipleOf`.
|
|
635
|
-
|
|
636
|
-
```ts
|
|
637
|
-
// Plage stricte ⊆ plage large
|
|
638
|
-
checker.isSubset(
|
|
639
|
-
{ type: "number", minimum: 5, maximum: 10 },
|
|
640
|
-
{ type: "number", minimum: 0, maximum: 100 }
|
|
641
|
-
); // true
|
|
642
|
-
|
|
643
|
-
// Plage large ⊄ plage stricte
|
|
644
|
-
checker.isSubset(
|
|
645
|
-
{ type: "number", minimum: 0, maximum: 100 },
|
|
646
|
-
{ type: "number", minimum: 5, maximum: 10 }
|
|
647
|
-
); // false
|
|
648
|
-
|
|
649
|
-
// exclusiveMinimum
|
|
650
|
-
checker.isSubset(
|
|
651
|
-
{ type: "number", exclusiveMinimum: 5 },
|
|
652
|
-
{ type: "number", exclusiveMinimum: 0 }
|
|
653
|
-
); // true (x > 5 implique x > 0)
|
|
654
|
-
|
|
655
|
-
// multipleOf : 6 est multiple de 3
|
|
656
|
-
checker.isSubset(
|
|
657
|
-
{ type: "number", multipleOf: 6 },
|
|
658
|
-
{ type: "number", multipleOf: 3 }
|
|
659
|
-
); // true
|
|
660
|
-
|
|
661
|
-
// multipleOf : 3 n'est PAS multiple de 6
|
|
662
|
-
checker.isSubset(
|
|
663
|
-
{ type: "number", multipleOf: 3 },
|
|
664
|
-
{ type: "number", multipleOf: 6 }
|
|
665
|
-
); // false
|
|
666
|
-
|
|
667
|
-
// L'intersection conserve les contraintes les plus restrictives
|
|
668
|
-
checker.intersect(
|
|
669
|
-
{ type: "number", minimum: 5, maximum: 10 },
|
|
670
|
-
{ type: "number", minimum: 0, maximum: 100 }
|
|
671
|
-
);
|
|
672
|
-
// → { type: "number", minimum: 5, maximum: 10 }
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
---
|
|
676
|
-
|
|
677
|
-
### 4. Contraintes de chaînes
|
|
678
|
-
|
|
679
|
-
Gestion de `minLength`, `maxLength` et `pattern`.
|
|
680
|
-
|
|
681
|
-
```ts
|
|
682
|
-
const strict = {
|
|
683
|
-
type: "string",
|
|
684
|
-
minLength: 3,
|
|
685
|
-
maxLength: 10,
|
|
686
|
-
pattern: "^[a-z]+$",
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
const loose = {
|
|
690
|
-
type: "string",
|
|
691
|
-
minLength: 1,
|
|
692
|
-
maxLength: 100,
|
|
197
|
+
properties: { accountId: { type: "string" } },
|
|
198
|
+
required: ["accountId"],
|
|
693
199
|
};
|
|
694
200
|
|
|
695
|
-
//
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
// Moins de contraintes → PAS sous-ensemble
|
|
699
|
-
checker.isSubset(loose, strict); // false
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
Pour les patterns regex, voir la section [12. Patterns regex](#12-patterns-regex-pattern).
|
|
703
|
-
|
|
704
|
-
---
|
|
705
|
-
|
|
706
|
-
### 5. `enum` et `const`
|
|
707
|
-
|
|
708
|
-
#### Enum
|
|
709
|
-
|
|
710
|
-
```ts
|
|
711
|
-
// Petit enum ⊆ grand enum (toutes les valeurs du petit sont dans le grand)
|
|
712
|
-
checker.isSubset(
|
|
713
|
-
{ type: "string", enum: ["a", "b"] },
|
|
714
|
-
{ type: "string", enum: ["a", "b", "c", "d"] }
|
|
715
|
-
); // true
|
|
716
|
-
|
|
717
|
-
// Grand enum ⊄ petit enum
|
|
718
|
-
checker.isSubset(
|
|
719
|
-
{ type: "string", enum: ["a", "b", "c", "d"] },
|
|
720
|
-
{ type: "string", enum: ["a", "b"] }
|
|
721
|
-
); // false
|
|
722
|
-
|
|
723
|
-
// Enum d'une seule valeur ⊆ type
|
|
724
|
-
checker.isSubset(
|
|
725
|
-
{ type: "string", enum: ["hello"] },
|
|
726
|
-
{ type: "string" }
|
|
727
|
-
); // true
|
|
728
|
-
|
|
729
|
-
// Intersection d'enums = valeurs communes
|
|
730
|
-
checker.intersect(
|
|
731
|
-
{ type: "string", enum: ["a", "b", "c"] },
|
|
732
|
-
{ type: "string", enum: ["b", "c", "d"] }
|
|
733
|
-
);
|
|
734
|
-
// → { type: "string", enum: ["b", "c"] }
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
#### Const
|
|
738
|
-
|
|
739
|
-
```ts
|
|
740
|
-
// const string ⊆ type string
|
|
741
|
-
checker.isSubset({ const: "hello" }, { type: "string" }); // true
|
|
742
|
-
|
|
743
|
-
// const number ⊆ type number
|
|
744
|
-
checker.isSubset({ const: 42 }, { type: "number" }); // true
|
|
201
|
+
// ❌ merge (intersection) : garde l'enum — FAUX pour un pipeline séquentiel
|
|
202
|
+
engine.merge(node1Output, node2Output);
|
|
203
|
+
// → { ..., properties: { accountId: { type: "string", enum: ["a", "b"] } } }
|
|
745
204
|
|
|
746
|
-
//
|
|
747
|
-
|
|
205
|
+
// ✅ overlay (deep spread) : le dernier écrivain gagne — CORRECT
|
|
206
|
+
engine.overlay(node1Output, node2Output);
|
|
207
|
+
// → { ..., properties: { accountId: { type: "string" } } }
|
|
748
208
|
```
|
|
749
209
|
|
|
750
|
-
|
|
210
|
+
👉 Pour la documentation complète de chaque méthode avec tous les exemples, consultez la **[Référence API](./docs/api-reference.md)**.
|
|
751
211
|
|
|
752
212
|
---
|
|
753
213
|
|
|
754
|
-
|
|
214
|
+
## 📖 Documentation complète
|
|
755
215
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const loose = {
|
|
767
|
-
type: "array",
|
|
768
|
-
items: { type: "string" },
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
// Tableau plus contraint ⊆ tableau moins contraint
|
|
772
|
-
checker.isSubset(strict, loose); // true
|
|
773
|
-
|
|
774
|
-
// L'inverse est faux
|
|
775
|
-
checker.isSubset(loose, strict); // false
|
|
776
|
-
|
|
777
|
-
// uniqueItems: true est plus restrictif que sans uniqueItems
|
|
778
|
-
checker.isSubset(
|
|
779
|
-
{ type: "array", items: { type: "number" }, uniqueItems: true },
|
|
780
|
-
{ type: "array", items: { type: "number" } }
|
|
781
|
-
); // true
|
|
782
|
-
```
|
|
216
|
+
| Page | Description |
|
|
217
|
+
|---|---|
|
|
218
|
+
| **[Référence API](./docs/api-reference.md)** | Documentation détaillée de chaque méthode (`JsonSchemaCompatibilityChecker` + `MergeEngine`) avec exemples |
|
|
219
|
+
| **[Guide des fonctionnalités](./docs/features-guide.md)** | Tour complet des fonctionnalités : types, `required`, contraintes numériques, `enum`/`const`, `anyOf`/`oneOf`, `not`, `format`, `pattern`, conditions `if/then/else`, `allOf`... |
|
|
220
|
+
| **[Fonctions utilitaires](./docs/utilities.md)** | `isPatternSubset`, `arePatternsEquivalent`, `isTrivialPattern` |
|
|
221
|
+
| **[Cas d'usage concrets](./docs/use-cases.md)** | Connexion de nœuds, pipeline séquentiel (overlay), validation de réponse API, unions discriminées, formulaires conditionnels |
|
|
222
|
+
| **[Types exportés](./docs/types.md)** | `SubsetResult`, `SchemaError`, `ResolvedConditionResult`, `ResolvedSubsetResult`, `CheckConditionsOptions` |
|
|
223
|
+
| **[Limitations connues](./docs/limitations.md)** | Cross-keyword constraints, `oneOf` exclusivité, patterns probabilistes, `$ref` non supporté |
|
|
224
|
+
| **[Architecture interne](./docs/architecture.md)** | Diagramme des modules, flux de vérification, merge vs overlay, dépendances |
|
|
783
225
|
|
|
784
226
|
---
|
|
785
227
|
|
|
786
|
-
|
|
228
|
+
## Limitations connues
|
|
787
229
|
|
|
788
|
-
|
|
230
|
+
- **Cross-keyword constraints** : `exclusiveMinimum` vs `minimum` peut produire des faux négatifs (limitation structurelle)
|
|
231
|
+
- **`oneOf` exclusivité** : traité comme `anyOf` — l'exclusivité sémantique n'est pas vérifiée
|
|
232
|
+
- **Patterns regex** : approche probabiliste par échantillonnage (200 samples), pas une preuve formelle
|
|
233
|
+
- **`if/then/else`** : nécessite des données discriminantes via `check(sub, sup, { subData })`
|
|
234
|
+
- **`$ref`** : non supporté — les schemas doivent être pré-déréférencés
|
|
235
|
+
- **`patternProperties`** : support partiel
|
|
789
236
|
|
|
790
|
-
|
|
791
|
-
const closed = {
|
|
792
|
-
type: "object",
|
|
793
|
-
properties: { name: { type: "string" } },
|
|
794
|
-
required: ["name"],
|
|
795
|
-
additionalProperties: false,
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
const open = {
|
|
799
|
-
type: "object",
|
|
800
|
-
properties: {
|
|
801
|
-
name: { type: "string" },
|
|
802
|
-
age: { type: "number" },
|
|
803
|
-
},
|
|
804
|
-
required: ["name"],
|
|
805
|
-
};
|
|
806
|
-
|
|
807
|
-
// Fermé ⊆ ouvert (un objet sans propriétés supplémentaires est valide partout)
|
|
808
|
-
checker.isSubset(closed, open); // true
|
|
809
|
-
|
|
810
|
-
// Ouvert ⊄ fermé (un objet avec age serait rejeté par closed)
|
|
811
|
-
checker.isSubset(open, closed); // false
|
|
812
|
-
|
|
813
|
-
// Le diagnostic montre la contrainte
|
|
814
|
-
const result = checker.check(open, closed);
|
|
815
|
-
console.log(result.errors);
|
|
816
|
-
// [{ key: "age", expected: "not allowed (additionalProperties: false)", received: "number" }]
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
---
|
|
820
|
-
|
|
821
|
-
### 8. Objets imbriqués
|
|
822
|
-
|
|
823
|
-
La vérification de sous-ensemble est **récursive** : elle descend dans toutes les propriétés imbriquées.
|
|
824
|
-
|
|
825
|
-
```ts
|
|
826
|
-
const deep = {
|
|
827
|
-
type: "object",
|
|
828
|
-
properties: {
|
|
829
|
-
user: {
|
|
830
|
-
type: "object",
|
|
831
|
-
properties: {
|
|
832
|
-
profile: {
|
|
833
|
-
type: "object",
|
|
834
|
-
properties: {
|
|
835
|
-
name: { type: "string" },
|
|
836
|
-
bio: { type: "string" },
|
|
837
|
-
},
|
|
838
|
-
required: ["name", "bio"],
|
|
839
|
-
},
|
|
840
|
-
},
|
|
841
|
-
required: ["profile"],
|
|
842
|
-
},
|
|
843
|
-
},
|
|
844
|
-
required: ["user"],
|
|
845
|
-
};
|
|
846
|
-
|
|
847
|
-
const shallow = {
|
|
848
|
-
type: "object",
|
|
849
|
-
properties: {
|
|
850
|
-
user: {
|
|
851
|
-
type: "object",
|
|
852
|
-
properties: {
|
|
853
|
-
profile: {
|
|
854
|
-
type: "object",
|
|
855
|
-
properties: { name: { type: "string" } },
|
|
856
|
-
required: ["name"],
|
|
857
|
-
},
|
|
858
|
-
},
|
|
859
|
-
required: ["profile"],
|
|
860
|
-
},
|
|
861
|
-
},
|
|
862
|
-
required: ["user"],
|
|
863
|
-
};
|
|
864
|
-
|
|
865
|
-
// Plus profond et plus exigeant → sous-ensemble du moins exigeant
|
|
866
|
-
checker.isSubset(deep, shallow); // true
|
|
867
|
-
checker.isSubset(shallow, deep); // false
|
|
868
|
-
|
|
869
|
-
// Les erreurs montrent les propriétés manquantes avec un chemin complet
|
|
870
|
-
const result = checker.check(shallow, deep);
|
|
871
|
-
console.log(result.errors);
|
|
872
|
-
// [{ key: "user.profile.bio", expected: "string", received: "undefined" }]
|
|
873
|
-
```
|
|
874
|
-
|
|
875
|
-
---
|
|
876
|
-
|
|
877
|
-
### 9. `anyOf` / `oneOf`
|
|
878
|
-
|
|
879
|
-
La librairie supporte `anyOf` et `oneOf` pour la vérification de sous-ensemble.
|
|
880
|
-
|
|
881
|
-
#### anyOf
|
|
882
|
-
|
|
883
|
-
```ts
|
|
884
|
-
const sub = {
|
|
885
|
-
anyOf: [{ type: "string" }, { type: "number" }],
|
|
886
|
-
};
|
|
887
|
-
const sup = {
|
|
888
|
-
anyOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }],
|
|
889
|
-
};
|
|
890
|
-
|
|
891
|
-
// Chaque branche de sub doit matcher une branche de sup
|
|
892
|
-
checker.isSubset(sub, sup); // true
|
|
893
|
-
checker.isSubset(sup, sub); // false
|
|
894
|
-
|
|
895
|
-
// Atomic ⊆ anyOf (si au moins une branche accepte)
|
|
896
|
-
checker.isSubset(
|
|
897
|
-
{ type: "string", minLength: 1 },
|
|
898
|
-
{ anyOf: [{ type: "string" }, { type: "number" }] }
|
|
899
|
-
); // true
|
|
900
|
-
|
|
901
|
-
// Atomic ⊄ anyOf (si aucune branche n'accepte)
|
|
902
|
-
checker.isSubset(
|
|
903
|
-
{ type: "boolean" },
|
|
904
|
-
{ anyOf: [{ type: "string" }, { type: "number" }] }
|
|
905
|
-
); // false
|
|
906
|
-
```
|
|
907
|
-
|
|
908
|
-
#### oneOf
|
|
909
|
-
|
|
910
|
-
Le `oneOf` est traité comme `anyOf` pour la vérification de sous-ensemble (chaque branche doit être acceptée).
|
|
911
|
-
|
|
912
|
-
```ts
|
|
913
|
-
const result = checker.check(
|
|
914
|
-
{ oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }] },
|
|
915
|
-
{ oneOf: [{ type: "string" }, { type: "number" }] }
|
|
916
|
-
);
|
|
917
|
-
|
|
918
|
-
console.log(result.isSubset); // false
|
|
919
|
-
console.log(result.errors); // erreurs pour la branche non couverte
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
#### Unions discriminées
|
|
923
|
-
|
|
924
|
-
```ts
|
|
925
|
-
const sub = {
|
|
926
|
-
oneOf: [
|
|
927
|
-
{
|
|
928
|
-
type: "object",
|
|
929
|
-
properties: { kind: { const: "a" }, value: { type: "string" } },
|
|
930
|
-
required: ["kind", "value"],
|
|
931
|
-
},
|
|
932
|
-
{
|
|
933
|
-
type: "object",
|
|
934
|
-
properties: { kind: { const: "b" }, value: { type: "number" } },
|
|
935
|
-
required: ["kind", "value"],
|
|
936
|
-
},
|
|
937
|
-
],
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
const sup = {
|
|
941
|
-
type: "object",
|
|
942
|
-
properties: { kind: { type: "string" } },
|
|
943
|
-
required: ["kind"],
|
|
944
|
-
};
|
|
945
|
-
|
|
946
|
-
// Chaque branche de l'union discriminée est sous-ensemble du sup
|
|
947
|
-
checker.isSubset(sub, sup); // true
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
> **Note** : La librairie ne vérifie **pas** l'exclusivité sémantique de `oneOf` (le fait qu'exactement une branche doit matcher). Elle traite `oneOf` comme `anyOf` pour la vérification de sous-ensemble.
|
|
951
|
-
|
|
952
|
-
---
|
|
953
|
-
|
|
954
|
-
### 10. Négation (`not`)
|
|
955
|
-
|
|
956
|
-
La librairie gère le mot-clé `not` avec un raisonnement étendu.
|
|
957
|
-
|
|
958
|
-
#### Cas de base
|
|
959
|
-
|
|
960
|
-
```ts
|
|
961
|
-
// number ⊆ not(string) → true (un nombre n'est jamais une string)
|
|
962
|
-
checker.isSubset(
|
|
963
|
-
{ type: "number" },
|
|
964
|
-
{ not: { type: "string" } }
|
|
965
|
-
); // true
|
|
966
|
-
|
|
967
|
-
// string ⊄ not(string) → false
|
|
968
|
-
checker.isSubset(
|
|
969
|
-
{ type: "string" },
|
|
970
|
-
{ not: { type: "string" } }
|
|
971
|
-
); // false
|
|
972
|
-
```
|
|
973
|
-
|
|
974
|
-
#### not avec `const` et `enum`
|
|
975
|
-
|
|
976
|
-
```ts
|
|
977
|
-
// status: "active" est compatible avec not(status: "deleted")
|
|
978
|
-
checker.isSubset(
|
|
979
|
-
{
|
|
980
|
-
type: "object",
|
|
981
|
-
properties: { status: { const: "active" } },
|
|
982
|
-
required: ["status"],
|
|
983
|
-
},
|
|
984
|
-
{
|
|
985
|
-
not: {
|
|
986
|
-
type: "object",
|
|
987
|
-
properties: { status: { const: "deleted" } },
|
|
988
|
-
required: ["status"],
|
|
989
|
-
},
|
|
990
|
-
}
|
|
991
|
-
); // true
|
|
992
|
-
|
|
993
|
-
// enum disjoint du not.enum → compatible
|
|
994
|
-
checker.isSubset(
|
|
995
|
-
{ enum: [1, 2] },
|
|
996
|
-
{ not: { enum: [3, 4] } }
|
|
997
|
-
); // true
|
|
998
|
-
|
|
999
|
-
// enum qui chevauche not.enum → incompatible
|
|
1000
|
-
checker.isSubset(
|
|
1001
|
-
{ enum: [1, 2, 3] },
|
|
1002
|
-
{ not: { enum: [3, 4] } }
|
|
1003
|
-
); // false
|
|
1004
|
-
```
|
|
1005
|
-
|
|
1006
|
-
#### not avec `anyOf` / `oneOf`
|
|
1007
|
-
|
|
1008
|
-
```ts
|
|
1009
|
-
// number est compatible avec not(anyOf([string, null]))
|
|
1010
|
-
checker.isSubset(
|
|
1011
|
-
{ type: "number" },
|
|
1012
|
-
{ not: { anyOf: [{ type: "string" }, { type: "null" }] } }
|
|
1013
|
-
); // true
|
|
1014
|
-
|
|
1015
|
-
// string est INcompatible avec not(anyOf([string, null]))
|
|
1016
|
-
checker.isSubset(
|
|
1017
|
-
{ type: "string" },
|
|
1018
|
-
{ not: { anyOf: [{ type: "string" }, { type: "null" }] } }
|
|
1019
|
-
); // false
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
#### Double négation
|
|
1023
|
-
|
|
1024
|
-
La normalisation résout automatiquement `not(not(X))` en `X` :
|
|
1025
|
-
|
|
1026
|
-
```ts
|
|
1027
|
-
// not(not(string)) normalise en string
|
|
1028
|
-
checker.normalize({ not: { not: { type: "string" } } });
|
|
1029
|
-
// → { type: "string" }
|
|
1030
|
-
|
|
1031
|
-
// Donc not(not(string)) ⊆ string
|
|
1032
|
-
checker.isSubset(
|
|
1033
|
-
{ not: { not: { type: "string", minLength: 3 } } },
|
|
1034
|
-
{ type: "string" }
|
|
1035
|
-
); // true
|
|
1036
|
-
```
|
|
1037
|
-
|
|
1038
|
-
#### `not` dans sub comme restriction
|
|
1039
|
-
|
|
1040
|
-
Quand `not` apparaît dans le sub, c'est une **restriction** (exclut des valeurs), donc le sub reste un sous-ensemble du sup :
|
|
1041
|
-
|
|
1042
|
-
```ts
|
|
1043
|
-
// { type: "string", not: { const: "foo" } } ⊆ { type: "string" }
|
|
1044
|
-
// "Toutes les strings sauf foo" est sous-ensemble de "toutes les strings"
|
|
1045
|
-
checker.isSubset(
|
|
1046
|
-
{ type: "string", not: { const: "foo" } },
|
|
1047
|
-
{ type: "string" }
|
|
1048
|
-
); // true
|
|
1049
|
-
```
|
|
1050
|
-
|
|
1051
|
-
---
|
|
1052
|
-
|
|
1053
|
-
### 11. Formats (`format`)
|
|
1054
|
-
|
|
1055
|
-
La librairie connaît les formats JSON Schema Draft-07 et leur hiérarchie d'inclusion.
|
|
1056
|
-
|
|
1057
|
-
#### Formats supportés
|
|
1058
|
-
|
|
1059
|
-
`date-time`, `date`, `time`, `email`, `idn-email`, `hostname`, `idn-hostname`, `ipv4`, `ipv6`, `uri`, `uri-reference`, `iri`, `iri-reference`, `uri-template`, `uuid`, `json-pointer`, `relative-json-pointer`, `regex`.
|
|
1060
|
-
|
|
1061
|
-
#### Hiérarchie des formats
|
|
1062
|
-
|
|
1063
|
-
```
|
|
1064
|
-
email ⊆ idn-email
|
|
1065
|
-
hostname ⊆ idn-hostname
|
|
1066
|
-
uri ⊆ iri
|
|
1067
|
-
uri-reference ⊆ iri-reference
|
|
1068
|
-
```
|
|
1069
|
-
|
|
1070
|
-
#### Exemples
|
|
1071
|
-
|
|
1072
|
-
```ts
|
|
1073
|
-
// format ⊆ type (email ⊆ string) → géré nativement par le merge
|
|
1074
|
-
checker.isSubset(
|
|
1075
|
-
{ type: "string", format: "email" },
|
|
1076
|
-
{ type: "string" }
|
|
1077
|
-
); // true
|
|
1078
|
-
|
|
1079
|
-
// type ⊄ format (string ⊄ email) → le format ajoute une contrainte
|
|
1080
|
-
checker.isSubset(
|
|
1081
|
-
{ type: "string" },
|
|
1082
|
-
{ type: "string", format: "email" }
|
|
1083
|
-
); // false
|
|
1084
|
-
|
|
1085
|
-
// Hiérarchie : email ⊆ idn-email
|
|
1086
|
-
checker.isSubset(
|
|
1087
|
-
{ type: "string", format: "email" },
|
|
1088
|
-
{ type: "string", format: "idn-email" }
|
|
1089
|
-
); // true
|
|
1090
|
-
|
|
1091
|
-
// Hiérarchie inverse : idn-email ⊄ email
|
|
1092
|
-
checker.isSubset(
|
|
1093
|
-
{ type: "string", format: "idn-email" },
|
|
1094
|
-
{ type: "string", format: "email" }
|
|
1095
|
-
); // false
|
|
1096
|
-
|
|
1097
|
-
// Formats incompatibles : email ∩ ipv4 = ∅
|
|
1098
|
-
checker.intersect(
|
|
1099
|
-
{ type: "string", format: "email" },
|
|
1100
|
-
{ type: "string", format: "ipv4" }
|
|
1101
|
-
); // null
|
|
1102
|
-
|
|
1103
|
-
// Même format : email ∩ email = email
|
|
1104
|
-
checker.intersect(
|
|
1105
|
-
{ type: "string", format: "email" },
|
|
1106
|
-
{ type: "string", format: "email" }
|
|
1107
|
-
);
|
|
1108
|
-
// → { type: "string", format: "email" }
|
|
1109
|
-
```
|
|
1110
|
-
|
|
1111
|
-
#### Formats dans les conditions
|
|
1112
|
-
|
|
1113
|
-
Les formats sont aussi évalués dans les conditions `if/then/else` via `class-validator` :
|
|
1114
|
-
|
|
1115
|
-
```ts
|
|
1116
|
-
const schema = {
|
|
1117
|
-
type: "object",
|
|
1118
|
-
properties: {
|
|
1119
|
-
contactMethod: { type: "string" },
|
|
1120
|
-
contactValue: { type: "string" },
|
|
1121
|
-
},
|
|
1122
|
-
if: {
|
|
1123
|
-
properties: { contactValue: { format: "email" } },
|
|
1124
|
-
},
|
|
1125
|
-
then: { required: ["contactValue"] },
|
|
1126
|
-
};
|
|
1127
|
-
|
|
1128
|
-
const result = checker.resolveConditions(schema, {
|
|
1129
|
-
contactValue: "test@example.com", // valide pour format: email
|
|
1130
|
-
});
|
|
1131
|
-
console.log(result.branch); // "then"
|
|
1132
|
-
```
|
|
1133
|
-
|
|
1134
|
-
---
|
|
1135
|
-
|
|
1136
|
-
### 12. Patterns regex (`pattern`)
|
|
1137
|
-
|
|
1138
|
-
Les patterns regex sont comparés via une approche par **échantillonnage** (sampling) pour détecter les inclusions.
|
|
1139
|
-
|
|
1140
|
-
#### Mêmes patterns
|
|
1141
|
-
|
|
1142
|
-
```ts
|
|
1143
|
-
// Même pattern → toujours sous-ensemble
|
|
1144
|
-
checker.isSubset(
|
|
1145
|
-
{ type: "string", pattern: "^[a-z]+$" },
|
|
1146
|
-
{ type: "string", pattern: "^[a-z]+$" }
|
|
1147
|
-
); // true
|
|
1148
|
-
```
|
|
1149
|
-
|
|
1150
|
-
#### Pattern plus restrictif ⊆ pattern plus permissif
|
|
1151
|
-
|
|
1152
|
-
```ts
|
|
1153
|
-
// ^[a-z]{3}$ ⊆ ^[a-z]+$ (3 lettres ⊆ 1+ lettres)
|
|
1154
|
-
checker.isSubset(
|
|
1155
|
-
{ type: "string", pattern: "^[a-z]{3}$" },
|
|
1156
|
-
{ type: "string", pattern: "^[a-z]+$" }
|
|
1157
|
-
); // true
|
|
1158
|
-
|
|
1159
|
-
// L'inverse est faux
|
|
1160
|
-
checker.isSubset(
|
|
1161
|
-
{ type: "string", pattern: "^[a-z]+$" },
|
|
1162
|
-
{ type: "string", pattern: "^[a-z]{3}$" }
|
|
1163
|
-
); // false
|
|
1164
|
-
```
|
|
1165
|
-
|
|
1166
|
-
#### Patterns incompatibles
|
|
1167
|
-
|
|
1168
|
-
```ts
|
|
1169
|
-
// Lettres ⊄ chiffres
|
|
1170
|
-
checker.isSubset(
|
|
1171
|
-
{ type: "string", pattern: "^[a-z]+$" },
|
|
1172
|
-
{ type: "string", pattern: "^[0-9]+$" }
|
|
1173
|
-
); // false
|
|
1174
|
-
```
|
|
1175
|
-
|
|
1176
|
-
#### Pattern vs pas de pattern
|
|
1177
|
-
|
|
1178
|
-
```ts
|
|
1179
|
-
// Sub avec pattern, sup sans pattern → sous-ensemble (sub plus restrictif)
|
|
1180
|
-
checker.isSubset(
|
|
1181
|
-
{ type: "string", pattern: "^[a-z]+$" },
|
|
1182
|
-
{ type: "string" }
|
|
1183
|
-
); // true
|
|
1184
|
-
|
|
1185
|
-
// Sub sans pattern, sup avec pattern → PAS sous-ensemble
|
|
1186
|
-
checker.isSubset(
|
|
1187
|
-
{ type: "string" },
|
|
1188
|
-
{ type: "string", pattern: "^[a-z]+$" }
|
|
1189
|
-
); // false
|
|
1190
|
-
```
|
|
1191
|
-
|
|
1192
|
-
#### Patterns dans les propriétés imbriquées
|
|
1193
|
-
|
|
1194
|
-
```ts
|
|
1195
|
-
// Pattern sur une propriété imbriquée
|
|
1196
|
-
checker.isSubset(
|
|
1197
|
-
{
|
|
1198
|
-
type: "object",
|
|
1199
|
-
properties: { code: { type: "string", pattern: "^FR[0-9]{5}$" } },
|
|
1200
|
-
required: ["code"],
|
|
1201
|
-
},
|
|
1202
|
-
{
|
|
1203
|
-
type: "object",
|
|
1204
|
-
properties: { code: { type: "string", pattern: "^[A-Z]{2}[0-9]+$" } },
|
|
1205
|
-
required: ["code"],
|
|
1206
|
-
}
|
|
1207
|
-
); // true (FR + 5 chiffres ⊆ 2 majuscules + chiffres)
|
|
1208
|
-
```
|
|
1209
|
-
|
|
1210
|
-
> **Note** : la comparaison de patterns utilise un échantillonnage avec 200 samples par défaut. C'est une heuristique, pas une preuve formelle. Les faux positifs sont possibles mais très improbables. Les faux négatifs (counter-examples concrets) sont certains.
|
|
1211
|
-
|
|
1212
|
-
---
|
|
1213
|
-
|
|
1214
|
-
### 13. Conditions `if` / `then` / `else`
|
|
1215
|
-
|
|
1216
|
-
La librairie peut résoudre les conditions JSON Schema en évaluant le `if` contre des données partielles.
|
|
1217
|
-
|
|
1218
|
-
#### Résolution simple
|
|
1219
|
-
|
|
1220
|
-
```ts
|
|
1221
|
-
const schema = {
|
|
1222
|
-
type: "object",
|
|
1223
|
-
properties: {
|
|
1224
|
-
status: { type: "string" },
|
|
1225
|
-
activatedAt: { type: "string", format: "date-time" },
|
|
1226
|
-
},
|
|
1227
|
-
required: ["status"],
|
|
1228
|
-
if: {
|
|
1229
|
-
properties: { status: { const: "active" } },
|
|
1230
|
-
required: ["status"],
|
|
1231
|
-
},
|
|
1232
|
-
then: {
|
|
1233
|
-
required: ["activatedAt"],
|
|
1234
|
-
},
|
|
1235
|
-
};
|
|
1236
|
-
|
|
1237
|
-
// Si status = "active" → branche then appliquée
|
|
1238
|
-
const active = checker.resolveConditions(schema, { status: "active" });
|
|
1239
|
-
console.log(active.branch); // "then"
|
|
1240
|
-
console.log(active.resolved.required); // ["status", "activatedAt"]
|
|
1241
|
-
|
|
1242
|
-
// Si status ≠ "active" → branche else (ou pas de branche supplémentaire)
|
|
1243
|
-
const inactive = checker.resolveConditions(schema, { status: "inactive" });
|
|
1244
|
-
console.log(inactive.branch); // "else"
|
|
1245
|
-
console.log(inactive.resolved.required); // ["status"]
|
|
1246
|
-
```
|
|
1247
|
-
|
|
1248
|
-
#### Résolution avec des conditions sur `enum`
|
|
1249
|
-
|
|
1250
|
-
```ts
|
|
1251
|
-
const schema = {
|
|
1252
|
-
type: "object",
|
|
1253
|
-
properties: {
|
|
1254
|
-
tier: { type: "string" },
|
|
1255
|
-
limit: { type: "number" },
|
|
1256
|
-
},
|
|
1257
|
-
required: ["tier"],
|
|
1258
|
-
if: {
|
|
1259
|
-
properties: { tier: { enum: ["premium", "enterprise"] } },
|
|
1260
|
-
required: ["tier"],
|
|
1261
|
-
},
|
|
1262
|
-
then: {
|
|
1263
|
-
properties: { limit: { type: "number", minimum: 1000 } },
|
|
1264
|
-
required: ["limit"],
|
|
1265
|
-
},
|
|
1266
|
-
else: {
|
|
1267
|
-
properties: { limit: { type: "number", maximum: 100 } },
|
|
1268
|
-
},
|
|
1269
|
-
};
|
|
1270
|
-
|
|
1271
|
-
const premium = checker.resolveConditions(schema, { tier: "premium" });
|
|
1272
|
-
console.log(premium.branch); // "then"
|
|
1273
|
-
// limit requis avec minimum 1000
|
|
1274
|
-
|
|
1275
|
-
const free = checker.resolveConditions(schema, { tier: "free" });
|
|
1276
|
-
console.log(free.branch); // "else"
|
|
1277
|
-
// limit optionnel avec maximum 100
|
|
1278
|
-
```
|
|
1279
|
-
|
|
1280
|
-
#### Conditions imbriquées dans les propriétés
|
|
1281
|
-
|
|
1282
|
-
La résolution est **récursive** : les conditions à l'intérieur des propriétés sont aussi résolues.
|
|
1283
|
-
|
|
1284
|
-
```ts
|
|
1285
|
-
const schema = {
|
|
1286
|
-
type: "object",
|
|
1287
|
-
properties: {
|
|
1288
|
-
config: {
|
|
1289
|
-
type: "object",
|
|
1290
|
-
properties: {
|
|
1291
|
-
mode: { type: "string", enum: ["fast", "safe"] },
|
|
1292
|
-
retries: { type: "number" },
|
|
1293
|
-
timeout: { type: "number" },
|
|
1294
|
-
},
|
|
1295
|
-
required: ["mode"],
|
|
1296
|
-
if: {
|
|
1297
|
-
properties: { mode: { const: "safe" } },
|
|
1298
|
-
required: ["mode"],
|
|
1299
|
-
},
|
|
1300
|
-
then: {
|
|
1301
|
-
required: ["retries", "timeout"],
|
|
1302
|
-
properties: {
|
|
1303
|
-
retries: { type: "number", minimum: 3 },
|
|
1304
|
-
timeout: { type: "number", minimum: 1000 },
|
|
1305
|
-
},
|
|
1306
|
-
},
|
|
1307
|
-
},
|
|
1308
|
-
},
|
|
1309
|
-
required: ["config"],
|
|
1310
|
-
};
|
|
1311
|
-
|
|
1312
|
-
const result = checker.resolveConditions(schema, {
|
|
1313
|
-
config: { mode: "safe" },
|
|
1314
|
-
});
|
|
1315
|
-
|
|
1316
|
-
// La condition dans config a été résolue
|
|
1317
|
-
const configProp = result.resolved.properties?.config;
|
|
1318
|
-
console.log(configProp?.required); // ["mode", "retries", "timeout"]
|
|
1319
|
-
```
|
|
1320
|
-
|
|
1321
|
-
#### Évaluation avancée du `if`
|
|
1322
|
-
|
|
1323
|
-
Le `if` est évalué contre les données avec support complet de :
|
|
1324
|
-
|
|
1325
|
-
| Mot-clé | Description |
|
|
1326
|
-
|---|---|
|
|
1327
|
-
| `properties` avec `const` | Correspondance exacte d'une valeur |
|
|
1328
|
-
| `properties` avec `enum` | Valeur dans une liste |
|
|
1329
|
-
| `properties` avec `type` | Vérification du type |
|
|
1330
|
-
| `required` | Présence des clés |
|
|
1331
|
-
| `allOf` | Toutes les conditions doivent matcher |
|
|
1332
|
-
| `anyOf` | Au moins une condition doit matcher |
|
|
1333
|
-
| `oneOf` | Exactement une condition doit matcher |
|
|
1334
|
-
| `not` | Inversion du résultat |
|
|
1335
|
-
| `format` | Validation sémantique via `class-validator` |
|
|
1336
|
-
| Contraintes numériques | `minimum`, `maximum`, `exclusiveMinimum`, etc. |
|
|
1337
|
-
| Contraintes string | `minLength`, `maxLength` |
|
|
1338
|
-
| Contraintes array | `minItems`, `maxItems`, `uniqueItems` |
|
|
1339
|
-
|
|
1340
|
-
---
|
|
1341
|
-
|
|
1342
|
-
### 14. `allOf` avec conditions
|
|
1343
|
-
|
|
1344
|
-
Les conditions peuvent apparaître dans un `allOf`. Chaque entrée contenant un `if/then/else` est résolue individuellement.
|
|
1345
|
-
|
|
1346
|
-
```ts
|
|
1347
|
-
const schema = {
|
|
1348
|
-
type: "object",
|
|
1349
|
-
properties: {
|
|
1350
|
-
name: { type: "string" },
|
|
1351
|
-
age: { type: "number" },
|
|
1352
|
-
role: { type: "string", enum: ["admin", "user", "guest"] },
|
|
1353
|
-
},
|
|
1354
|
-
required: ["name"],
|
|
1355
|
-
allOf: [
|
|
1356
|
-
{
|
|
1357
|
-
if: {
|
|
1358
|
-
properties: { age: { type: "number", exclusiveMinimum: 20 } },
|
|
1359
|
-
required: ["age"],
|
|
1360
|
-
},
|
|
1361
|
-
then: {
|
|
1362
|
-
required: ["email"],
|
|
1363
|
-
properties: { email: { type: "string" } },
|
|
1364
|
-
},
|
|
1365
|
-
},
|
|
1366
|
-
{
|
|
1367
|
-
if: {
|
|
1368
|
-
properties: { role: { const: "admin" } },
|
|
1369
|
-
required: ["role"],
|
|
1370
|
-
},
|
|
1371
|
-
then: {
|
|
1372
|
-
properties: {
|
|
1373
|
-
permissions: { type: "array", items: { type: "string" } },
|
|
1374
|
-
},
|
|
1375
|
-
required: ["permissions"],
|
|
1376
|
-
},
|
|
1377
|
-
},
|
|
1378
|
-
],
|
|
1379
|
-
};
|
|
1380
|
-
|
|
1381
|
-
// Les deux conditions matchent
|
|
1382
|
-
const result = checker.resolveConditions(schema, {
|
|
1383
|
-
name: "Alice",
|
|
1384
|
-
age: 25,
|
|
1385
|
-
role: "admin",
|
|
1386
|
-
});
|
|
1387
|
-
|
|
1388
|
-
console.log(result.resolved.required);
|
|
1389
|
-
// → ["name", "email", "permissions"]
|
|
1390
|
-
// email requis car age > 20, permissions requis car role = admin
|
|
1391
|
-
|
|
1392
|
-
console.log(result.resolved.properties?.email);
|
|
1393
|
-
// → { type: "string" }
|
|
1394
|
-
|
|
1395
|
-
console.log(result.resolved.properties?.permissions);
|
|
1396
|
-
// → { type: "array", items: { type: "string" } }
|
|
1397
|
-
```
|
|
1398
|
-
|
|
1399
|
-
#### `allOf` combiné avec un `if/then/else` au niveau racine
|
|
1400
|
-
|
|
1401
|
-
```ts
|
|
1402
|
-
const schema = {
|
|
1403
|
-
type: "object",
|
|
1404
|
-
properties: {
|
|
1405
|
-
kind: { type: "string" },
|
|
1406
|
-
value: {},
|
|
1407
|
-
},
|
|
1408
|
-
required: ["kind"],
|
|
1409
|
-
// Condition racine
|
|
1410
|
-
if: {
|
|
1411
|
-
properties: { kind: { const: "numeric" } },
|
|
1412
|
-
required: ["kind"],
|
|
1413
|
-
},
|
|
1414
|
-
then: {
|
|
1415
|
-
properties: { value: { type: "number" } },
|
|
1416
|
-
},
|
|
1417
|
-
else: {
|
|
1418
|
-
properties: { value: { type: "string" } },
|
|
1419
|
-
},
|
|
1420
|
-
// Condition dans allOf
|
|
1421
|
-
allOf: [
|
|
1422
|
-
{
|
|
1423
|
-
if: {
|
|
1424
|
-
properties: { kind: { const: "numeric" } },
|
|
1425
|
-
required: ["kind"],
|
|
1426
|
-
},
|
|
1427
|
-
then: {
|
|
1428
|
-
properties: { precision: { type: "number" } },
|
|
1429
|
-
},
|
|
1430
|
-
},
|
|
1431
|
-
],
|
|
1432
|
-
};
|
|
1433
|
-
|
|
1434
|
-
const result = checker.resolveConditions(schema, { kind: "numeric" });
|
|
1435
|
-
|
|
1436
|
-
// Les deux conditions (racine + allOf) sont résolues
|
|
1437
|
-
console.log(result.resolved.properties?.value); // { type: "number" }
|
|
1438
|
-
console.log(result.resolved.properties?.precision); // { type: "number" }
|
|
1439
|
-
```
|
|
1440
|
-
|
|
1441
|
-
---
|
|
1442
|
-
|
|
1443
|
-
## Fonctions utilitaires
|
|
1444
|
-
|
|
1445
|
-
En plus de la classe principale, la librairie exporte des fonctions utilitaires pour travailler avec les patterns regex.
|
|
1446
|
-
|
|
1447
|
-
```ts
|
|
1448
|
-
import {
|
|
1449
|
-
isPatternSubset,
|
|
1450
|
-
arePatternsEquivalent,
|
|
1451
|
-
isTrivialPattern,
|
|
1452
|
-
} from "json-schema-compatibility-checker";
|
|
1453
|
-
```
|
|
1454
|
-
|
|
1455
|
-
---
|
|
1456
|
-
|
|
1457
|
-
### `isPatternSubset(sub, sup)`
|
|
1458
|
-
|
|
1459
|
-
```ts
|
|
1460
|
-
isPatternSubset(
|
|
1461
|
-
subPattern: string,
|
|
1462
|
-
supPattern: string,
|
|
1463
|
-
sampleCount?: number // défaut: 200
|
|
1464
|
-
): boolean | null
|
|
1465
|
-
```
|
|
1466
|
-
|
|
1467
|
-
Vérifie si le langage du pattern `sub` est un sous-ensemble du langage du pattern `sup` via **échantillonnage**.
|
|
1468
|
-
|
|
1469
|
-
**Contrat ternaire :**
|
|
1470
|
-
- `true` — toutes les strings échantillonnées de sub matchent sup (confiance haute)
|
|
1471
|
-
- `false` — au moins une string de sub ne matche PAS sup (certain, c'est un contre-exemple)
|
|
1472
|
-
- `null` — impossible de déterminer (pattern invalide, génération échouée)
|
|
1473
|
-
|
|
1474
|
-
```ts
|
|
1475
|
-
import { isPatternSubset } from "json-schema-compatibility-checker";
|
|
1476
|
-
|
|
1477
|
-
isPatternSubset("^[a-z]{3}$", "^[a-z]+$"); // true — 3 lettres ⊆ 1+ lettres
|
|
1478
|
-
isPatternSubset("^[a-z]+$", "^[0-9]+$"); // false — lettres ⊄ chiffres
|
|
1479
|
-
isPatternSubset("^[a-z]+$", "^[a-z]{3}$"); // false — "ab" matche sub mais pas sup
|
|
1480
|
-
isPatternSubset("invalid[", "^[a-z]+$"); // null — pattern invalide
|
|
1481
|
-
|
|
1482
|
-
// Cas réalistes
|
|
1483
|
-
isPatternSubset("^SKU-[0-9]{6}$", "^[A-Z]+-[0-9]+$"); // true
|
|
1484
|
-
isPatternSubset("^FR[0-9]{5}$", "^[A-Z]{2}[0-9]+$"); // true
|
|
1485
|
-
isPatternSubset("^(75|92|93|94)[0-9]{3}$", "^[0-9]{5}$"); // true
|
|
1486
|
-
```
|
|
1487
|
-
|
|
1488
|
-
---
|
|
1489
|
-
|
|
1490
|
-
### `arePatternsEquivalent(a, b)`
|
|
1491
|
-
|
|
1492
|
-
```ts
|
|
1493
|
-
arePatternsEquivalent(
|
|
1494
|
-
patternA: string,
|
|
1495
|
-
patternB: string,
|
|
1496
|
-
sampleCount?: number // défaut: 200
|
|
1497
|
-
): boolean | null
|
|
1498
|
-
```
|
|
1499
|
-
|
|
1500
|
-
Vérifie si deux patterns acceptent le **même langage** via un échantillonnage bidirectionnel (`A ⊆ B` ET `B ⊆ A`).
|
|
1501
|
-
|
|
1502
|
-
```ts
|
|
1503
|
-
import { arePatternsEquivalent } from "json-schema-compatibility-checker";
|
|
1504
|
-
|
|
1505
|
-
arePatternsEquivalent("^[a-z]+$", "^[a-z]+$"); // true — identiques
|
|
1506
|
-
arePatternsEquivalent("^[a-z]+$", "^[a-z]{3}$"); // false — cardinalité différente
|
|
1507
|
-
arePatternsEquivalent("^[a-f]+$", "^[a-z]+$"); // false — a-f ⊆ a-z mais pas l'inverse
|
|
1508
|
-
```
|
|
1509
|
-
|
|
1510
|
-
---
|
|
1511
|
-
|
|
1512
|
-
### `isTrivialPattern(pattern)`
|
|
1513
|
-
|
|
1514
|
-
```ts
|
|
1515
|
-
isTrivialPattern(pattern: string): boolean
|
|
1516
|
-
```
|
|
1517
|
-
|
|
1518
|
-
Vérifie si un pattern est **universellement permissif** (matche toute string). Utile pour détecter les patterns qui n'ajoutent aucune contrainte réelle.
|
|
1519
|
-
|
|
1520
|
-
```ts
|
|
1521
|
-
import { isTrivialPattern } from "json-schema-compatibility-checker";
|
|
1522
|
-
|
|
1523
|
-
isTrivialPattern(".*"); // true
|
|
1524
|
-
isTrivialPattern(".+"); // true
|
|
1525
|
-
isTrivialPattern("^.*$"); // true
|
|
1526
|
-
isTrivialPattern("^.+$"); // true
|
|
1527
|
-
isTrivialPattern(""); // true (pattern vide)
|
|
1528
|
-
|
|
1529
|
-
isTrivialPattern("^[a-z]+$"); // false
|
|
1530
|
-
isTrivialPattern("^[0-9]{3}$"); // false
|
|
1531
|
-
isTrivialPattern("abc"); // false
|
|
1532
|
-
```
|
|
1533
|
-
|
|
1534
|
-
---
|
|
1535
|
-
|
|
1536
|
-
## Cas d'usage concrets
|
|
1537
|
-
|
|
1538
|
-
### Connexion de nœuds dans un orchestrateur
|
|
1539
|
-
|
|
1540
|
-
Le cas d'usage principal de la librairie : dans un système d'orchestration visuel (style n8n, Node-RED, Zapier), vérifier que la sortie d'un nœud est compatible avec l'entrée du suivant.
|
|
1541
|
-
|
|
1542
|
-
```ts
|
|
1543
|
-
const checker = new JsonSchemaCompatibilityChecker();
|
|
1544
|
-
|
|
1545
|
-
// Nœud A : API qui retourne des utilisateurs paginés
|
|
1546
|
-
const nodeAOutput = {
|
|
1547
|
-
type: "object",
|
|
1548
|
-
properties: {
|
|
1549
|
-
items: {
|
|
1550
|
-
type: "array",
|
|
1551
|
-
items: {
|
|
1552
|
-
type: "object",
|
|
1553
|
-
properties: {
|
|
1554
|
-
id: { type: "integer", minimum: 1 },
|
|
1555
|
-
name: { type: "string", minLength: 1, maxLength: 255 },
|
|
1556
|
-
tags: {
|
|
1557
|
-
type: "array",
|
|
1558
|
-
items: { type: "string" },
|
|
1559
|
-
uniqueItems: true,
|
|
1560
|
-
},
|
|
1561
|
-
},
|
|
1562
|
-
required: ["id", "name"],
|
|
1563
|
-
},
|
|
1564
|
-
},
|
|
1565
|
-
page: { type: "integer", minimum: 1 },
|
|
1566
|
-
pageSize: { type: "integer", minimum: 1, maximum: 100 },
|
|
1567
|
-
totalPages: { type: "integer", minimum: 0 },
|
|
1568
|
-
},
|
|
1569
|
-
required: ["items", "page", "pageSize", "totalPages"],
|
|
1570
|
-
};
|
|
1571
|
-
|
|
1572
|
-
// Nœud B : traitement qui attend une liste avec pagination
|
|
1573
|
-
const nodeBInput = {
|
|
1574
|
-
type: "object",
|
|
1575
|
-
properties: {
|
|
1576
|
-
items: {
|
|
1577
|
-
type: "array",
|
|
1578
|
-
items: {
|
|
1579
|
-
type: "object",
|
|
1580
|
-
properties: {
|
|
1581
|
-
id: { type: "number" },
|
|
1582
|
-
name: { type: "string" },
|
|
1583
|
-
},
|
|
1584
|
-
required: ["id"],
|
|
1585
|
-
},
|
|
1586
|
-
},
|
|
1587
|
-
page: { type: "number" },
|
|
1588
|
-
totalPages: { type: "number" },
|
|
1589
|
-
},
|
|
1590
|
-
required: ["items"],
|
|
1591
|
-
};
|
|
1592
|
-
|
|
1593
|
-
const result = checker.check(nodeAOutput, nodeBInput);
|
|
1594
|
-
console.log(result.isSubset); // true ✅
|
|
1595
|
-
|
|
1596
|
-
// Si incompatible, le diagnostic explique pourquoi
|
|
1597
|
-
if (!result.isSubset) {
|
|
1598
|
-
console.log(checker.formatResult("NodeA → NodeB", result));
|
|
1599
|
-
}
|
|
1600
|
-
```
|
|
1601
|
-
|
|
1602
|
-
---
|
|
1603
|
-
|
|
1604
|
-
### Validation de réponse API
|
|
1605
|
-
|
|
1606
|
-
Vérifier qu'une réponse API réelle est compatible avec ce qu'un consommateur attend.
|
|
1607
|
-
|
|
1608
|
-
```ts
|
|
1609
|
-
const apiResponse = {
|
|
1610
|
-
type: "object",
|
|
1611
|
-
properties: {
|
|
1612
|
-
status: { type: "integer", minimum: 100, maximum: 599 },
|
|
1613
|
-
data: {
|
|
1614
|
-
type: "object",
|
|
1615
|
-
properties: {
|
|
1616
|
-
users: {
|
|
1617
|
-
type: "array",
|
|
1618
|
-
items: {
|
|
1619
|
-
type: "object",
|
|
1620
|
-
properties: {
|
|
1621
|
-
id: { type: "string", format: "uuid" },
|
|
1622
|
-
email: { type: "string", format: "email" },
|
|
1623
|
-
name: { type: "string", minLength: 1 },
|
|
1624
|
-
role: { type: "string", enum: ["admin", "user", "viewer"] },
|
|
1625
|
-
},
|
|
1626
|
-
required: ["id", "email", "name", "role"],
|
|
1627
|
-
},
|
|
1628
|
-
},
|
|
1629
|
-
total: { type: "integer", minimum: 0 },
|
|
1630
|
-
},
|
|
1631
|
-
required: ["users", "total"],
|
|
1632
|
-
},
|
|
1633
|
-
},
|
|
1634
|
-
required: ["status", "data"],
|
|
1635
|
-
};
|
|
1636
|
-
|
|
1637
|
-
const consumerExpects = {
|
|
1638
|
-
type: "object",
|
|
1639
|
-
properties: {
|
|
1640
|
-
status: { type: "integer" },
|
|
1641
|
-
data: {
|
|
1642
|
-
type: "object",
|
|
1643
|
-
properties: {
|
|
1644
|
-
users: {
|
|
1645
|
-
type: "array",
|
|
1646
|
-
items: {
|
|
1647
|
-
type: "object",
|
|
1648
|
-
properties: {
|
|
1649
|
-
id: { type: "string" },
|
|
1650
|
-
email: { type: "string" },
|
|
1651
|
-
},
|
|
1652
|
-
required: ["id", "email"],
|
|
1653
|
-
},
|
|
1654
|
-
},
|
|
1655
|
-
},
|
|
1656
|
-
required: ["users"],
|
|
1657
|
-
},
|
|
1658
|
-
},
|
|
1659
|
-
required: ["data"],
|
|
1660
|
-
};
|
|
1661
|
-
|
|
1662
|
-
const result = checker.check(apiResponse, consumerExpects);
|
|
1663
|
-
console.log(result.isSubset); // true ✅
|
|
1664
|
-
// L'API retourne plus de données que ce que le consommateur attend,
|
|
1665
|
-
// mais TOUTES les données requises sont présentes et du bon type.
|
|
1666
|
-
```
|
|
1667
|
-
|
|
1668
|
-
---
|
|
1669
|
-
|
|
1670
|
-
### Union discriminée
|
|
1671
|
-
|
|
1672
|
-
Vérifier qu'une union discriminée (`oneOf` avec un champ discriminant) est compatible avec un schema d'entrée flexible.
|
|
1673
|
-
|
|
1674
|
-
```ts
|
|
1675
|
-
const output = {
|
|
1676
|
-
oneOf: [
|
|
1677
|
-
{
|
|
1678
|
-
type: "object",
|
|
1679
|
-
properties: {
|
|
1680
|
-
type: { const: "success" },
|
|
1681
|
-
data: { type: "object" },
|
|
1682
|
-
},
|
|
1683
|
-
required: ["type", "data"],
|
|
1684
|
-
},
|
|
1685
|
-
{
|
|
1686
|
-
type: "object",
|
|
1687
|
-
properties: {
|
|
1688
|
-
type: { const: "error" },
|
|
1689
|
-
message: { type: "string" },
|
|
1690
|
-
code: { type: "integer" },
|
|
1691
|
-
},
|
|
1692
|
-
required: ["type", "message"],
|
|
1693
|
-
},
|
|
1694
|
-
],
|
|
1695
|
-
};
|
|
1696
|
-
|
|
1697
|
-
const input = {
|
|
1698
|
-
type: "object",
|
|
1699
|
-
properties: {
|
|
1700
|
-
type: { type: "string" },
|
|
1701
|
-
},
|
|
1702
|
-
required: ["type"],
|
|
1703
|
-
};
|
|
1704
|
-
|
|
1705
|
-
// Chaque branche de l'union a un champ "type" de type string
|
|
1706
|
-
checker.isSubset(output, input); // true ✅
|
|
1707
|
-
```
|
|
1708
|
-
|
|
1709
|
-
---
|
|
1710
|
-
|
|
1711
|
-
### Formulaire conditionnel
|
|
1712
|
-
|
|
1713
|
-
Valider qu'un formulaire rempli par l'utilisateur est compatible avec un schema conditionnel.
|
|
1714
|
-
|
|
1715
|
-
```ts
|
|
1716
|
-
const formSchema = {
|
|
1717
|
-
type: "object",
|
|
1718
|
-
properties: {
|
|
1719
|
-
accountType: { type: "string", enum: ["personal", "business"] },
|
|
1720
|
-
email: { type: "string", format: "email" },
|
|
1721
|
-
companyName: { type: "string" },
|
|
1722
|
-
taxId: { type: "string" },
|
|
1723
|
-
firstName: { type: "string" },
|
|
1724
|
-
lastName: { type: "string" },
|
|
1725
|
-
},
|
|
1726
|
-
required: ["accountType", "email"],
|
|
1727
|
-
if: {
|
|
1728
|
-
properties: { accountType: { const: "business" } },
|
|
1729
|
-
required: ["accountType"],
|
|
1730
|
-
},
|
|
1731
|
-
then: { required: ["companyName", "taxId"] },
|
|
1732
|
-
else: { required: ["firstName", "lastName"] },
|
|
1733
|
-
};
|
|
1734
|
-
|
|
1735
|
-
// Output d'un formulaire "business" rempli
|
|
1736
|
-
const businessOutput = {
|
|
1737
|
-
type: "object",
|
|
1738
|
-
properties: {
|
|
1739
|
-
accountType: { const: "business", type: "string", enum: ["personal", "business"] },
|
|
1740
|
-
email: { type: "string", format: "email" },
|
|
1741
|
-
companyName: { type: "string", minLength: 1 },
|
|
1742
|
-
taxId: { type: "string", minLength: 1 },
|
|
1743
|
-
},
|
|
1744
|
-
required: ["accountType", "email", "companyName", "taxId"],
|
|
1745
|
-
additionalProperties: false,
|
|
1746
|
-
};
|
|
1747
|
-
|
|
1748
|
-
// Sans résolution, le if/then/else brut cause un faux négatif
|
|
1749
|
-
checker.isSubset(businessOutput, formSchema); // false ❌
|
|
1750
|
-
|
|
1751
|
-
// Avec résolution, le schéma conditionnel est aplati
|
|
1752
|
-
const result = checker.check(businessOutput, formSchema, {
|
|
1753
|
-
subData: { accountType: "business" },
|
|
1754
|
-
});
|
|
1755
|
-
console.log(result.isSubset); // true ✅
|
|
1756
|
-
console.log(result.resolvedSup.branch); // "then"
|
|
1757
|
-
|
|
1758
|
-
// Output d'un formulaire "personal" rempli
|
|
1759
|
-
const personalOutput = {
|
|
1760
|
-
type: "object",
|
|
1761
|
-
properties: {
|
|
1762
|
-
accountType: { const: "personal", type: "string", enum: ["personal", "business"] },
|
|
1763
|
-
email: { type: "string", format: "email" },
|
|
1764
|
-
firstName: { type: "string", minLength: 1 },
|
|
1765
|
-
lastName: { type: "string", minLength: 1 },
|
|
1766
|
-
},
|
|
1767
|
-
required: ["accountType", "email", "firstName", "lastName"],
|
|
1768
|
-
additionalProperties: false,
|
|
1769
|
-
};
|
|
1770
|
-
|
|
1771
|
-
const personalResult = checker.check(personalOutput, formSchema, {
|
|
1772
|
-
subData: { accountType: "personal" },
|
|
1773
|
-
});
|
|
1774
|
-
console.log(personalResult.isSubset); // true ✅
|
|
1775
|
-
console.log(personalResult.resolvedSup.branch); // "else"
|
|
1776
|
-
```
|
|
1777
|
-
|
|
1778
|
-
---
|
|
1779
|
-
|
|
1780
|
-
## Types exportés
|
|
1781
|
-
|
|
1782
|
-
```ts
|
|
1783
|
-
import type {
|
|
1784
|
-
SubsetResult,
|
|
1785
|
-
SchemaError,
|
|
1786
|
-
ResolvedConditionResult,
|
|
1787
|
-
ResolvedSubsetResult,
|
|
1788
|
-
CheckConditionsOptions,
|
|
1789
|
-
} from "json-schema-compatibility-checker";
|
|
1790
|
-
```
|
|
1791
|
-
|
|
1792
|
-
### `SchemaError`
|
|
1793
|
-
|
|
1794
|
-
```ts
|
|
1795
|
-
interface SchemaError {
|
|
1796
|
-
/** Chemin normalisé vers la propriété concernée (ex: "user.name", "users[].name", "accountId") */
|
|
1797
|
-
key: string;
|
|
1798
|
-
/** Type ou valeur attendu(e) par le schema cible (sup) */
|
|
1799
|
-
expected: string;
|
|
1800
|
-
/** Type ou valeur reçu(e) depuis le schema source (sub) */
|
|
1801
|
-
received: string;
|
|
1802
|
-
}
|
|
1803
|
-
```
|
|
1804
|
-
|
|
1805
|
-
### `SubsetResult`
|
|
1806
|
-
|
|
1807
|
-
```ts
|
|
1808
|
-
interface SubsetResult {
|
|
1809
|
-
/** true si sub ⊆ sup */
|
|
1810
|
-
isSubset: boolean;
|
|
1811
|
-
/** Le schema résultant de l'intersection allOf(sub, sup), ou null si incompatible */
|
|
1812
|
-
merged: JSONSchema7Definition | null;
|
|
1813
|
-
/** Erreurs sémantiques décrivant les incompatibilités entre les deux schemas */
|
|
1814
|
-
errors: SchemaError[];
|
|
1815
|
-
}
|
|
1816
|
-
```
|
|
1817
|
-
|
|
1818
|
-
### `ResolvedConditionResult`
|
|
1819
|
-
|
|
1820
|
-
```ts
|
|
1821
|
-
interface ResolvedConditionResult {
|
|
1822
|
-
/** Le schema avec les if/then/else résolus (aplatis) */
|
|
1823
|
-
resolved: JSONSchema7;
|
|
1824
|
-
/** La branche qui a été appliquée ("then" | "else" | null si pas de condition) */
|
|
1825
|
-
branch: "then" | "else" | null;
|
|
1826
|
-
/** Le discriminant utilisé pour résoudre */
|
|
1827
|
-
discriminant: Record<string, unknown>;
|
|
1828
|
-
}
|
|
1829
|
-
```
|
|
1830
|
-
|
|
1831
|
-
### `ResolvedSubsetResult`
|
|
1832
|
-
|
|
1833
|
-
```ts
|
|
1834
|
-
interface ResolvedSubsetResult extends SubsetResult {
|
|
1835
|
-
/** Résultat de résolution des conditions du sub */
|
|
1836
|
-
resolvedSub: ResolvedConditionResult;
|
|
1837
|
-
/** Résultat de résolution des conditions du sup */
|
|
1838
|
-
resolvedSup: ResolvedConditionResult;
|
|
1839
|
-
}
|
|
1840
|
-
```
|
|
1841
|
-
|
|
1842
|
-
### `CheckConditionsOptions`
|
|
1843
|
-
|
|
1844
|
-
```ts
|
|
1845
|
-
interface CheckConditionsOptions {
|
|
1846
|
-
/** Runtime data for the sub schema — used for condition resolution and enum narrowing */
|
|
1847
|
-
subData: unknown;
|
|
1848
|
-
/** Runtime data for the sup schema (defaults to subData) — used for condition resolution and enum narrowing */
|
|
1849
|
-
supData?: unknown;
|
|
1850
|
-
}
|
|
1851
|
-
```
|
|
1852
|
-
|
|
1853
|
-
---
|
|
1854
|
-
|
|
1855
|
-
## Limitations connues
|
|
1856
|
-
|
|
1857
|
-
### 1. Cross-keyword constraints
|
|
1858
|
-
|
|
1859
|
-
La librairie utilise une comparaison **structurelle** : elle compare les mots-clés individuellement. Elle ne peut pas raisonner sur des relations entre mots-clés différents mais sémantiquement liés.
|
|
1860
|
-
|
|
1861
|
-
```ts
|
|
1862
|
-
// Sémantiquement, {exclusiveMinimum: 5} ⊆ {minimum: 0} est VRAI (x>5 implique x≥0)
|
|
1863
|
-
// Mais le merge ajoute minimum:0, ce qui rend merged ≠ sub structurellement
|
|
1864
|
-
checker.isSubset(
|
|
1865
|
-
{ type: "number", exclusiveMinimum: 5 },
|
|
1866
|
-
{ type: "number", minimum: 0 }
|
|
1867
|
-
); // false (faux négatif)
|
|
1868
|
-
```
|
|
1869
|
-
|
|
1870
|
-
### 2. `oneOf` — exclusivité non vérifiée
|
|
1871
|
-
|
|
1872
|
-
La librairie traite `oneOf` comme `anyOf` pour la vérification de sous-ensemble. L'exclusivité sémantique (exactement une branche doit matcher) n'est **pas** vérifiée.
|
|
1873
|
-
|
|
1874
|
-
```ts
|
|
1875
|
-
const overlapping = {
|
|
1876
|
-
oneOf: [
|
|
1877
|
-
{ type: "string", minLength: 1 }, // branches qui se chevauchent
|
|
1878
|
-
{ type: "string", maxLength: 100 },
|
|
1879
|
-
],
|
|
1880
|
-
};
|
|
1881
|
-
// En strict oneOf, "abc" matcherait les DEUX branches → rejeté
|
|
1882
|
-
// La librairie ne détecte pas ce chevauchement
|
|
1883
|
-
```
|
|
1884
|
-
|
|
1885
|
-
### 3. Patterns regex — approche probabiliste
|
|
1886
|
-
|
|
1887
|
-
La comparaison de patterns regex utilise un **échantillonnage** (200 samples par défaut). C'est une heuristique, pas une preuve formelle.
|
|
1888
|
-
|
|
1889
|
-
- **Faux négatifs** certains : si un counter-example est trouvé, l'exclusion est garantie
|
|
1890
|
-
- **Faux positifs** possibles : si tous les échantillons passent, ce n'est pas une preuve formelle (mais très improbable avec 200 samples)
|
|
1891
|
-
- Les patterns avec backreferences complexes peuvent poser problème
|
|
1892
|
-
|
|
1893
|
-
### 4. `if/then/else` — nécessite des données discriminantes
|
|
1894
|
-
|
|
1895
|
-
Les schemas avec `if/then/else` ne peuvent pas être comparés directement via `isSubset` car le merge brut ajoute les mots-clés conditionnels. Il faut utiliser `check(sub, sup, { subData })` avec les données discriminantes.
|
|
1896
|
-
|
|
1897
|
-
### 5. `$ref` — non supporté
|
|
1898
|
-
|
|
1899
|
-
Les références `$ref` ne sont pas résolues par la librairie. Il faut dé-référencer le schema avant de l'utiliser.
|
|
1900
|
-
|
|
1901
|
-
### 6. `patternProperties` — support partiel
|
|
1902
|
-
|
|
1903
|
-
Les `patternProperties` sont normalisés et comparés structurellement, mais la comparaison sémantique des patterns comme clés n'est pas effectuée.
|
|
1904
|
-
|
|
1905
|
-
---
|
|
1906
|
-
|
|
1907
|
-
## Architecture interne
|
|
1908
|
-
|
|
1909
|
-
La librairie est organisée en modules spécialisés, orchestrés par la façade `JsonSchemaCompatibilityChecker` :
|
|
1910
|
-
|
|
1911
|
-
```
|
|
1912
|
-
┌──────────────────────────────────────────────────┐
|
|
1913
|
-
│ JsonSchemaCompatibilityChecker │
|
|
1914
|
-
│ (Façade) │
|
|
1915
|
-
├──────────────────────────────────────────────────┤
|
|
1916
|
-
│ │
|
|
1917
|
-
│ ┌──────────────┐ ┌──────────────────────────┐ │
|
|
1918
|
-
│ │ Normalizer │ │ Condition Resolver │ │
|
|
1919
|
-
│ │ │ │ │ │
|
|
1920
|
-
│ │ - Infer type │ │ - Evaluate if │ │
|
|
1921
|
-
│ │ - enum→const │ │ - Merge then/else │ │
|
|
1922
|
-
│ │ - not(not(X))│ │ - Recurse in allOf │ │
|
|
1923
|
-
│ │ - Recurse │ │ - Nested properties │ │
|
|
1924
|
-
│ └──────────────┘ └──────────────────────────┘ │
|
|
1925
|
-
│ │
|
|
1926
|
-
│ ┌──────────────┐ ┌──────────────────────────┐ │
|
|
1927
|
-
│ │ Merge Engine │ │ Subset Checker │ │
|
|
1928
|
-
│ │ │ │ │ │
|
|
1929
|
-
│ │ - allOf merge│ │ - Atomic: A∩B ≡ A ? │ │
|
|
1930
|
-
│ │ - Conflict │ │ - Branched sub (anyOf) │ │
|
|
1931
|
-
│ │ detection │ │ - Branched sup (anyOf) │ │
|
|
1932
|
-
│ │ - Compare │ │ - evaluateNot │ │
|
|
1933
|
-
│ └──────────────┘ │ - stripNotFromSup │ │
|
|
1934
|
-
│ │ - stripPatternFromSup │ │
|
|
1935
|
-
│ ┌──────────────┐ └──────────────────────────┘ │
|
|
1936
|
-
│ │Semantic Errors│ │
|
|
1937
|
-
│ │ │ ┌──────────────────────────┐ │
|
|
1938
|
-
│ │-computeErrors│ │ Pattern Subset │ │
|
|
1939
|
-
│ │ - Recurse │ │ │ │
|
|
1940
|
-
│ │ - Properties │ │ - isPatternSubset │ │
|
|
1941
|
-
│ └──────────────┘ │ - arePatternsEquivalent │ │
|
|
1942
|
-
│ │ - isTrivialPattern │ │
|
|
1943
|
-
│ ┌──────────────┐ └──────────────────────────┘ │
|
|
1944
|
-
│ │ Formatter │ │
|
|
1945
|
-
│ │ │ ┌──────────────────────────┐ │
|
|
1946
|
-
│ │ - formatResult│ │ Format Validator │ │
|
|
1947
|
-
│ │ - Error lines│ │ │ │
|
|
1948
|
-
│ └──────────────┘ │ - validateFormat │ │
|
|
1949
|
-
│ │ - isFormatSubset │ │
|
|
1950
|
-
│ ┌──────────────┐ │ - Format hierarchy │ │
|
|
1951
|
-
│ │Data Narrowing │ └──────────────────────────┘ │
|
|
1952
|
-
│ │ │ │
|
|
1953
|
-
│ │-narrowSchema │ │
|
|
1954
|
-
│ │ WithData │ │
|
|
1955
|
-
│ │ - enum match │ │
|
|
1956
|
-
│ │ - Recurse │ │
|
|
1957
|
-
│ └──────────────┘ │
|
|
1958
|
-
└──────────────────────────────────────────────────┘
|
|
1959
|
-
```
|
|
1960
|
-
|
|
1961
|
-
### Flux de vérification `isSubset(sub, sup)`
|
|
1962
|
-
|
|
1963
|
-
```
|
|
1964
|
-
1. Normalize(sub), Normalize(sup)
|
|
1965
|
-
2. Detect branches (anyOf/oneOf) in sub and sup
|
|
1966
|
-
3. For each branch combination:
|
|
1967
|
-
a. evaluateNot() — pre-check not compatibility
|
|
1968
|
-
b. stripNotFromSup() — remove compatible not constraints
|
|
1969
|
-
c. stripPatternFromSup() — handle pattern inclusion via sampling
|
|
1970
|
-
d. engine.merge(sub, sup) — compute intersection
|
|
1971
|
-
e. normalize(merged)
|
|
1972
|
-
f. engine.isEqual(normalized_sub, normalized_merged) ?
|
|
1973
|
-
→ true: sub ⊆ sup ✅
|
|
1974
|
-
→ false: compute diffs, sub ⊄ sup ❌
|
|
1975
|
-
```
|
|
1976
|
-
|
|
1977
|
-
### Dépendances
|
|
1978
|
-
|
|
1979
|
-
| Package | Usage |
|
|
1980
|
-
|---|---|
|
|
1981
|
-
| `@x0k/json-schema-merge` | Merge engine pour `allOf` resolution |
|
|
1982
|
-
| `class-validator` | Validation des formats (email, URL, UUID, etc.) |
|
|
1983
|
-
| `randexp` | Génération de strings pour le sampling de patterns |
|
|
237
|
+
👉 Détails et exemples dans **[Limitations connues](./docs/limitations.md)**.
|
|
1984
238
|
|
|
1985
239
|
---
|
|
1986
240
|
|
|
1987
241
|
## Licence
|
|
1988
242
|
|
|
1989
|
-
MIT
|
|
243
|
+
MIT
|