json-schema-compatibility-checker 1.0.6 → 1.0.7

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
@@ -15,9 +15,8 @@
15
15
  - [`check(sub, sup)`](#checksub-sup)
16
16
  - [`isEqual(a, b)`](#isequala-b)
17
17
  - [`intersect(a, b)`](#intersecta-b)
18
- - [`canConnect(sourceOutput, targetInput)`](#canconnectsourceoutput-targetinput)
19
18
  - [`resolveConditions(schema, data)`](#resolveconditionsschema-data)
20
- - [`checkResolved(sub, sup, subData, supData?)`](#checkresolvedsub-sup-subdata-supdata)
19
+ - [`check(sub, sup, options)`](#checksub-sup-options)
21
20
  - [`normalize(schema)`](#normalizeschema)
22
21
  - [`formatResult(label, result)`](#formatresultlabel-result)
23
22
  - [Guide des fonctionnalités](#guide-des-fonctionnalités)
@@ -203,22 +202,24 @@ checker.isSubset({ type: "string" }, true); // → true
203
202
 
204
203
  ```ts
205
204
  check(sub: JSONSchema7Definition, sup: JSONSchema7Definition): SubsetResult
205
+ check(sub: JSONSchema7Definition, sup: JSONSchema7Definition, options: CheckConditionsOptions): ResolvedSubsetResult
206
206
  ```
207
207
 
208
- Comme `isSubset`, mais retourne un **résultat détaillé** avec les différences structurelles.
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).
209
211
 
210
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
+
211
219
  interface SubsetResult {
212
220
  isSubset: boolean;
213
221
  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
+ errors: SchemaError[]; // Erreurs sémantiques
222
223
  }
223
224
  ```
224
225
 
@@ -231,7 +232,7 @@ const result = checker.check(
231
232
  );
232
233
 
233
234
  console.log(result.isSubset); // true
234
- console.log(result.diffs); // [] (aucune différence)
235
+ console.log(result.errors); // [] (aucune erreur)
235
236
  console.log(result.merged); // { type: "string", minLength: 5 }
236
237
  ```
237
238
 
@@ -254,12 +255,13 @@ const sup = {
254
255
  };
255
256
 
256
257
  const result = checker.check(sub, sup);
258
+ console.log(result.errors);
259
+ // [{ key: "age", expected: "number", received: "undefined" }]
257
260
 
258
261
  console.log(result.isSubset); // false
259
- console.log(result.diffs);
262
+ console.log(result.errors);
260
263
  // [
261
- // { path: "required", type: "changed", expected: ["name"], actual: ["name", "age"] },
262
- // { path: "properties.age", type: "added", expected: undefined, actual: { type: "number" } }
264
+ // { key: "age", expected: "number", received: "undefined" }
263
265
  // ]
264
266
  ```
265
267
 
@@ -270,7 +272,7 @@ const result = checker.check({ type: "string" }, { type: "number" });
270
272
 
271
273
  console.log(result.isSubset); // false
272
274
  console.log(result.merged); // null (intersection impossible)
273
- console.log(result.diffs); // [{ path: "$", type: "changed", expected: ..., actual: "Incompatible..." }]
275
+ console.log(result.errors); // [{ key: "$root", expected: "number", received: "string" }]
274
276
  ```
275
277
 
276
278
  ---
@@ -363,64 +365,6 @@ checker.intersect({ type: "string" }, { type: "number" });
363
365
 
364
366
  ---
365
367
 
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
368
  ### `resolveConditions(schema, data)`
425
369
 
426
370
  ```ts
@@ -481,22 +425,32 @@ console.log(personal.resolved.required);
481
425
 
482
426
  ---
483
427
 
484
- ### `checkResolved(sub, sup, subData, supData?)`
428
+ ### `check(sub, sup, options)`
485
429
 
486
430
  ```ts
487
- checkResolved(
488
- sub: JSONSchema7,
489
- sup: JSONSchema7,
490
- subData: Record<string, unknown>,
491
- supData?: Record<string, unknown>
492
- ): SubsetResult & {
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 {
493
449
  resolvedSub: ResolvedConditionResult;
494
450
  resolvedSup: ResolvedConditionResult;
495
451
  }
496
452
  ```
497
453
 
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
454
  ```ts
501
455
  const conditionalSup = {
502
456
  type: "object",
@@ -529,10 +483,16 @@ const sub = {
529
483
  // Sans résolution : false (le if/then/else brut ne matche pas)
530
484
  console.log(checker.isSubset(sub, conditionalSup)); // false
531
485
 
532
- // Avec résolution : true !
533
- const result = checker.checkResolved(sub, conditionalSup, { kind: "text" });
486
+ // Avec résolution via options : true !
487
+ const result = checker.check(sub, conditionalSup, { subData: { kind: "text" } });
534
488
  console.log(result.isSubset); // true ✅
535
489
  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
+ });
536
496
  ```
537
497
 
538
498
  ---
@@ -581,9 +541,8 @@ const result = checker.check(
581
541
 
582
542
  console.log(checker.formatResult("range check", result));
583
543
  // ❌ range check: false
584
- // Diffs:
585
- // ~ minimum: 0 5
586
- // ~ maximum: 100 → 10
544
+ // Errors:
545
+ // $root: expected minimum 5, received minimum 0
587
546
  ```
588
547
 
589
548
  ```ts
@@ -596,10 +555,10 @@ console.log(checker.formatResult("strict ⊆ loose", result2));
596
555
  // ✅ strict ⊆ loose: true
597
556
  ```
598
557
 
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)
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
603
562
 
604
563
  ---
605
564
 
@@ -664,11 +623,8 @@ checker.isSubset(loose, strict); // false
664
623
 
665
624
  // Le diagnostic montre exactement ce qui manque
666
625
  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
- // ]
626
+ console.log(result.errors);
627
+ // [{ key: "age", expected: "number", received: "undefined" }]
672
628
  ```
673
629
 
674
630
  ---
@@ -856,8 +812,8 @@ checker.isSubset(open, closed); // false
856
812
 
857
813
  // Le diagnostic montre la contrainte
858
814
  const result = checker.check(open, closed);
859
- const addPropDiff = result.diffs.find(d => d.path === "additionalProperties");
860
- console.log(addPropDiff); // { path: "additionalProperties", type: "added", ... }
815
+ console.log(result.errors);
816
+ // [{ key: "age", expected: "not allowed (additionalProperties: false)", received: "number" }]
861
817
  ```
862
818
 
863
819
  ---
@@ -910,19 +866,17 @@ const shallow = {
910
866
  checker.isSubset(deep, shallow); // true
911
867
  checker.isSubset(shallow, deep); // false
912
868
 
913
- // Les chemins de diff sont complets
869
+ // Les erreurs montrent les propriétés manquantes avec un chemin complet
914
870
  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"
871
+ console.log(result.errors);
872
+ // [{ key: "user.profile.bio", expected: "string", received: "undefined" }]
919
873
  ```
920
874
 
921
875
  ---
922
876
 
923
877
  ### 9. `anyOf` / `oneOf`
924
878
 
925
- La librairie supporte `anyOf` et `oneOf` avec distinction dans les chemins de diff.
879
+ La librairie supporte `anyOf` et `oneOf` pour la vérification de sous-ensemble.
926
880
 
927
881
  #### anyOf
928
882
 
@@ -953,16 +907,16 @@ checker.isSubset(
953
907
 
954
908
  #### oneOf
955
909
 
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**.
910
+ Le `oneOf` est traité comme `anyOf` pour la vérification de sous-ensemble (chaque branche doit être acceptée).
957
911
 
958
912
  ```ts
959
- // Les chemins de diff utilisent le bon label
960
913
  const result = checker.check(
961
914
  { oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }] },
962
915
  { oneOf: [{ type: "string" }, { type: "number" }] }
963
916
  );
964
917
 
965
- result.diffs[0].path; // "oneOf[2]" (et non "anyOf[2]")
918
+ console.log(result.isSubset); // false
919
+ console.log(result.errors); // erreurs pour la branche non couverte
966
920
  ```
967
921
 
968
922
  #### Unions discriminées
@@ -1636,13 +1590,12 @@ const nodeBInput = {
1636
1590
  required: ["items"],
1637
1591
  };
1638
1592
 
1639
- const connection = checker.canConnect(nodeAOutput, nodeBInput);
1640
- console.log(connection.isSubset); // true ✅
1641
- console.log(connection.direction); // "sourceOutput ⊆ targetInput"
1593
+ const result = checker.check(nodeAOutput, nodeBInput);
1594
+ console.log(result.isSubset); // true ✅
1642
1595
 
1643
1596
  // Si incompatible, le diagnostic explique pourquoi
1644
- if (!connection.isSubset) {
1645
- console.log(checker.formatResult("NodeA → NodeB", connection));
1597
+ if (!result.isSubset) {
1598
+ console.log(checker.formatResult("NodeA → NodeB", result));
1646
1599
  }
1647
1600
  ```
1648
1601
 
@@ -1706,7 +1659,7 @@ const consumerExpects = {
1706
1659
  required: ["data"],
1707
1660
  };
1708
1661
 
1709
- const result = checker.canConnect(apiResponse, consumerExpects);
1662
+ const result = checker.check(apiResponse, consumerExpects);
1710
1663
  console.log(result.isSubset); // true ✅
1711
1664
  // L'API retourne plus de données que ce que le consommateur attend,
1712
1665
  // mais TOUTES les données requises sont présentes et du bon type.
@@ -1796,8 +1749,8 @@ const businessOutput = {
1796
1749
  checker.isSubset(businessOutput, formSchema); // false ❌
1797
1750
 
1798
1751
  // Avec résolution, le schéma conditionnel est aplati
1799
- const result = checker.checkResolved(businessOutput, formSchema, {
1800
- accountType: "business",
1752
+ const result = checker.check(businessOutput, formSchema, {
1753
+ subData: { accountType: "business" },
1801
1754
  });
1802
1755
  console.log(result.isSubset); // true ✅
1803
1756
  console.log(result.resolvedSup.branch); // "then"
@@ -1815,8 +1768,8 @@ const personalOutput = {
1815
1768
  additionalProperties: false,
1816
1769
  };
1817
1770
 
1818
- const personalResult = checker.checkResolved(personalOutput, formSchema, {
1819
- accountType: "personal",
1771
+ const personalResult = checker.check(personalOutput, formSchema, {
1772
+ subData: { accountType: "personal" },
1820
1773
  });
1821
1774
  console.log(personalResult.isSubset); // true ✅
1822
1775
  console.log(personalResult.resolvedSup.branch); // "else"
@@ -1829,26 +1782,23 @@ console.log(personalResult.resolvedSup.branch); // "else"
1829
1782
  ```ts
1830
1783
  import type {
1831
1784
  SubsetResult,
1832
- ConnectionResult,
1785
+ SchemaError,
1833
1786
  ResolvedConditionResult,
1834
- SchemaDiff,
1835
- BranchType,
1836
- BranchResult,
1787
+ ResolvedSubsetResult,
1788
+ CheckConditionsOptions,
1837
1789
  } from "json-schema-compatibility-checker";
1838
1790
  ```
1839
1791
 
1840
- ### `SchemaDiff`
1792
+ ### `SchemaError`
1841
1793
 
1842
1794
  ```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;
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;
1852
1802
  }
1853
1803
  ```
1854
1804
 
@@ -1860,17 +1810,8 @@ interface SubsetResult {
1860
1810
  isSubset: boolean;
1861
1811
  /** Le schema résultant de l'intersection allOf(sub, sup), ou null si incompatible */
1862
1812
  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;
1813
+ /** Erreurs sémantiques décrivant les incompatibilités entre les deux schemas */
1814
+ errors: SchemaError[];
1874
1815
  }
1875
1816
  ```
1876
1817
 
@@ -1887,6 +1828,28 @@ interface ResolvedConditionResult {
1887
1828
  }
1888
1829
  ```
1889
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
+
1890
1853
  ---
1891
1854
 
1892
1855
  ## Limitations connues
@@ -1929,7 +1892,7 @@ La comparaison de patterns regex utilise un **échantillonnage** (200 samples pa
1929
1892
 
1930
1893
  ### 4. `if/then/else` — nécessite des données discriminantes
1931
1894
 
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.
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.
1933
1896
 
1934
1897
  ### 5. `$ref` — non supporté
1935
1898
 
@@ -1970,9 +1933,9 @@ La librairie est organisée en modules spécialisés, orchestrés par la façade
1970
1933
  │ └──────────────┘ │ - stripNotFromSup │ │
1971
1934
  │ │ - stripPatternFromSup │ │
1972
1935
  │ ┌──────────────┐ └──────────────────────────┘ │
1973
- │ │ Differ │ │
1936
+ │ │Semantic Errors│ │
1974
1937
  │ │ │ ┌──────────────────────────┐ │
1975
- │ │ - computeDiff│ │ Pattern Subset │ │
1938
+ │-computeErrors│ │ Pattern Subset │ │
1976
1939
  │ │ - Recurse │ │ │ │
1977
1940
  │ │ - Properties │ │ - isPatternSubset │ │
1978
1941
  │ └──────────────┘ │ - arePatternsEquivalent │ │
@@ -1981,11 +1944,17 @@ La librairie est organisée en modules spécialisés, orchestrés par la façade
1981
1944
  │ │ Formatter │ │
1982
1945
  │ │ │ ┌──────────────────────────┐ │
1983
1946
  │ │ - formatResult│ │ Format Validator │ │
1984
- │ │ - Diff lines │ │ │ │
1947
+ │ │ - Error lines│ │ │ │
1985
1948
  │ └──────────────┘ │ - validateFormat │ │
1986
1949
  │ │ - isFormatSubset │ │
1987
- │ - Format hierarchy │ │
1988
- └──────────────────────────┘ │
1950
+ ┌──────────────┐ │ - Format hierarchy │ │
1951
+ │Data Narrowing │ └──────────────────────────┘ │
1952
+ │ │ │ │
1953
+ │ │-narrowSchema │ │
1954
+ │ │ WithData │ │
1955
+ │ │ - enum match │ │
1956
+ │ │ - Recurse │ │
1957
+ │ └──────────────┘ │
1989
1958
  └──────────────────────────────────────────────────┘
1990
1959
  ```
1991
1960
 
@@ -2010,7 +1979,6 @@ La librairie est organisée en modules spécialisés, orchestrés par la façade
2010
1979
  | Package | Usage |
2011
1980
  |---|---|
2012
1981
  | `@x0k/json-schema-merge` | Merge engine pour `allOf` resolution |
2013
- | `lodash` | Utilitaires (isEqual, mapValues, union, etc.) |
2014
1982
  | `class-validator` | Validation des formats (email, URL, UUID, etc.) |
2015
1983
  | `randexp` | Génération de strings pour le sampling de patterns |
2016
1984
 
@@ -2018,4 +1986,4 @@ La librairie est organisée en modules spécialisés, orchestrés par la façade
2018
1986
 
2019
1987
  ## Licence
2020
1988
 
2021
- Projet privé.
1989
+ MIT