json-schema-compatibility-checker 1.0.7 → 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.
Files changed (2) hide show
  1. package/README.md +39 -1836
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -11,41 +11,9 @@
11
11
  - [Installation](#installation)
12
12
  - [Démarrage rapide](#démarrage-rapide)
13
13
  - [API Reference](#api-reference)
14
- - [`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
 
@@ -151,1839 +119,74 @@ console.log(checker.isSubset(loose, strict)); // false ❌
151
119
  Toutes les méthodes sont exposées par la classe `JsonSchemaCompatibilityChecker`.
152
120
 
153
121
  ```ts
154
- import { JsonSchemaCompatibilityChecker } from "json-schema-compatibility-checker";
155
-
156
122
  const checker = new JsonSchemaCompatibilityChecker();
157
123
  ```
158
124
 
159
- ---
160
-
161
- ### `isSubset(sub, sup)`
162
-
163
- ```ts
164
- isSubset(sub: JSONSchema7Definition, sup: JSONSchema7Definition): boolean
165
- ```
166
-
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
179
-
180
- // string n'est PAS un sous-ensemble de number
181
- checker.isSubset({ type: "string" }, { type: "number" });
182
- // → false
183
- ```
184
-
185
- #### Schemas booléens
186
-
187
- ```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)`
202
-
203
- ```ts
204
- check(sub: JSONSchema7Definition, sup: JSONSchema7Definition): SubsetResult
205
- check(sub: JSONSchema7Definition, sup: JSONSchema7Definition, options: CheckConditionsOptions): ResolvedSubsetResult
206
- ```
207
-
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
- ```
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` |
225
135
 
226
- #### Exemple — Check compatible
136
+ **Exemple rapide `check` avec diagnostic :**
227
137
 
228
138
  ```ts
229
139
  const result = checker.check(
230
- { type: "string", minLength: 5 },
231
- { type: "string" }
140
+ { type: "object", properties: { name: { type: "string" } }, required: ["name"] },
141
+ { type: "object", properties: { name: { type: "string" }, age: { type: "number" } }, required: ["name", "age"] }
232
142
  );
233
143
 
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
144
  console.log(result.isSubset); // false
262
145
  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
146
+ // [{ key: "age", expected: "number", received: "undefined" }]
375
147
  ```
376
148
 
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`.
149
+ **Exemple rapide résolution de conditions :**
378
150
 
379
151
  ```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
- ```
386
-
387
- ```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",
152
+ const result = checker.check(sub, conditionalSup, {
153
+ subData: { kind: "text" },
420
154
  });
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
155
  console.log(result.isSubset); // true ✅
489
156
  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
- ```
533
-
534
- Formate un `SubsetResult` en chaîne lisible pour les logs / le debug.
535
-
536
- ```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
- ---
570
-
571
- ### 1. Compatibilité de types
572
-
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 = {
602
- type: "object",
603
- properties: {
604
- name: { type: "string" },
605
- age: { type: "number" },
606
- },
607
- required: ["name", "age"],
608
- };
609
-
610
- const loose = {
611
- 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
157
  ```
629
158
 
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
- ```
159
+ 👉 Pour la documentation complète de chaque méthode avec tous les exemples, consultez la **[Référence API](./docs/api-reference.md)**.
674
160
 
675
161
  ---
676
162
 
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,
693
- };
694
-
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
- ```
163
+ ## 📖 Documentation complète
701
164
 
702
- Pour les patterns regex, voir la section [12. Patterns regex](#12-patterns-regex-pattern).
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 |
703
174
 
704
175
  ---
705
176
 
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
177
+ ## Limitations connues
728
178
 
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
- ```
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
736
185
 
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
745
-
746
- // const string ⊄ type number (types incompatibles)
747
- checker.isSubset({ const: "hello" }, { type: "number" }); // false
748
- ```
749
-
750
- > **Normalisation** : un `enum` à un seul élément est automatiquement converti en `const` lors de la normalisation. `{ enum: ["x"] }` ≡ `{ const: "x" }`.
751
-
752
- ---
753
-
754
- ### 6. Contraintes de tableaux
755
-
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
- ```
783
-
784
- ---
785
-
786
- ### 7. `additionalProperties`
787
-
788
- `additionalProperties: false` ferme un objet : seules les propriétés listées dans `properties` sont autorisées.
789
-
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 |
186
+ 👉 Détails et exemples dans **[Limitations connues](./docs/limitations.md)**.
1984
187
 
1985
188
  ---
1986
189
 
1987
190
  ## Licence
1988
191
 
1989
- MIT
192
+ MIT