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 CHANGED
@@ -11,41 +11,9 @@
11
11
  - [Installation](#installation)
12
12
  - [Démarrage rapide](#démarrage-rapide)
13
13
  - [API Reference](#api-reference)
14
- - [`isSubset(sub, sup)`](#issubsetsub-sup)
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
- - [Architecture interne](#architecture-interne)
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
- Toutes les méthodes sont exposées par la classe `JsonSchemaCompatibilityChecker`.
120
+ ### `JsonSchemaCompatibilityChecker`
152
121
 
153
- ```ts
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
- isSubset(sub: JSONSchema7Definition, sup: JSONSchema7Definition): boolean
125
+ const checker = new JsonSchemaCompatibilityChecker();
165
126
  ```
166
127
 
167
- Vérifie si `sub sup` c'est-à-dire si **toute valeur valide pour `sub` est aussi valide pour `sup`**.
168
-
169
- Retourne un simple `boolean`.
170
-
171
- ```ts
172
- // integer est un sous-ensemble de number
173
- checker.isSubset({ type: "integer" }, { type: "number" });
174
- // true
175
-
176
- // number n'est PAS un sous-ensemble de integer
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
- // string n'est PAS un sous-ensemble de number
181
- checker.isSubset({ type: "string" }, { type: "number" });
182
- // → false
183
- ```
139
+ ### `MergeEngine`
184
140
 
185
- #### Schemas booléens
141
+ Opérations bas-niveau sur les schemas : intersection (`allOf` merge) et overlay (deep spread séquentiel).
186
142
 
187
143
  ```ts
188
- // false (aucune valeur) est sous-ensemble de tout
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
- ```ts
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
- Comme `isSubset`, mais retourne un **résultat détaillé** avec les erreurs sémantiques.
209
-
210
- Quand `options` est fourni, les conditions `if/then/else` sont résolues avant le check (voir [`check(sub, sup, options)`](#checksub-sup-options) plus bas).
211
-
212
- ```ts
213
- interface SchemaError {
214
- key: string; // Chemin normalisé (ex: "user.name", "users[].email")
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
- #### Exemple — Check compatible
157
+ **Exemple rapide `check` avec diagnostic :**
227
158
 
228
159
  ```ts
229
160
  const result = checker.check(
230
- { type: "string", minLength: 5 },
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
- Résout les `if/then/else` d'un schema en évaluant le `if` contre des données partielles (discriminants). Le schema résultant est un schema "aplati" sans `if/then/else`.
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 formSchema = {
389
- type: "object",
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
- Formate un `SubsetResult` en chaîne lisible pour les logs / le debug.
180
+ **Exemple rapide `overlay` pour accumulation séquentielle :**
535
181
 
536
182
  ```ts
537
- const result = checker.check(
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
- ### 1. Compatibilité de types
185
+ const engine = new MergeEngine();
572
186
 
573
- La librairie comprend le système de types JSON Schema et ses relations d'inclusion.
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
- name: { type: "string" },
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
- const loose = {
194
+ // Node2 redéfinit accountId en string simple (plus large)
195
+ const node2Output = {
611
196
  type: "object",
612
- properties: {
613
- name: { type: "string" },
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
- // Plus de contraintes sous-ensemble
696
- checker.isSubset(strict, loose); // true
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
- // const string type number (types incompatibles)
747
- checker.isSubset({ const: "hello" }, { type: "number" }); // false
205
+ // overlay (deep spread) : le dernier écrivain gagne — CORRECT
206
+ engine.overlay(node1Output, node2Output);
207
+ // → { ..., properties: { accountId: { type: "string" } } }
748
208
  ```
749
209
 
750
- > **Normalisation** : un `enum` à un seul élément est automatiquement converti en `const` lors de la normalisation. `{ enum: ["x"] }` ≡ `{ const: "x" }`.
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
- ### 6. Contraintes de tableaux
214
+ ## 📖 Documentation complète
755
215
 
756
- Gestion de `items`, `minItems`, `maxItems`, `uniqueItems`.
757
-
758
- ```ts
759
- const strict = {
760
- type: "array",
761
- items: { type: "string", minLength: 1 },
762
- minItems: 1,
763
- maxItems: 5,
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
- ### 7. `additionalProperties`
228
+ ## Limitations connues
787
229
 
788
- `additionalProperties: false` ferme un objet : seules les propriétés listées dans `properties` sont autorisées.
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
- ```ts
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