json-schema-compatibility-checker 1.0.6 → 1.0.8

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