json-schema-utils 0.8__tar.gz → 0.8.2__tar.gz

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 (23) hide show
  1. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/Makefile +1 -1
  2. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/PKG-INFO +4 -2
  3. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/README.md +3 -1
  4. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/PKG-INFO +4 -2
  5. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/convert.py +74 -55
  6. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/scripts.py +11 -7
  7. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/simplify.py +32 -11
  8. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/stats.py +11 -0
  9. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/pyproject.toml +1 -1
  10. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/.gitignore +0 -0
  11. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/LICENSE +0 -0
  12. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/MANIFEST.in +0 -0
  13. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/SOURCES.txt +0 -0
  14. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/dependency_links.txt +0 -0
  15. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/entry_points.txt +0 -0
  16. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/requires.txt +0 -0
  17. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/top_level.txt +0 -0
  18. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/__init__.py +0 -0
  19. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/inline.py +0 -0
  20. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/recurse.py +0 -0
  21. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/schemas.py +0 -0
  22. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/utils.py +0 -0
  23. {json_schema_utils-0.8 → json_schema_utils-0.8.2}/setup.cfg +0 -0
@@ -39,7 +39,7 @@ check.ruff: venv
39
39
  .PHONY: check.flake8
40
40
  check.flake8: venv
41
41
  source venv/bin/activate
42
- flake8 --ignore=$(IGNORE),W504 --max-line-length=100 jsutils
42
+ flake8 --ignore=$(IGNORE),E128,E131,W504 --max-line-length=100 jsutils
43
43
 
44
44
  .PHONY: check.pyright
45
45
  check.pyright: venv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: json_schema_utils
3
- Version: 0.8
3
+ Version: 0.8.2
4
4
  Summary: JSON Schema Utils
5
5
  Author: Fabien Coelho, Claire Yannou-Medrala
6
6
  License-Expression: CC0-1.0
@@ -28,7 +28,9 @@ Random utilities to analyze and manipulate JSON Schema.
28
28
 
29
29
  ## Installation
30
30
 
31
- Install latest version with `pip` from PyPI or from GitHub:
31
+ Install latest version with `pip` from
32
+ [PyPI](https://pypi.org/project/json-schema-utils) or directly from
33
+ [GitHub](https://github.com/zx80/json-schema-utils):
32
34
 
33
35
  ```sh
34
36
  python -m venv venv
@@ -4,7 +4,9 @@ Random utilities to analyze and manipulate JSON Schema.
4
4
 
5
5
  ## Installation
6
6
 
7
- Install latest version with `pip` from PyPI or from GitHub:
7
+ Install latest version with `pip` from
8
+ [PyPI](https://pypi.org/project/json-schema-utils) or directly from
9
+ [GitHub](https://github.com/zx80/json-schema-utils):
8
10
 
9
11
  ```sh
10
12
  python -m venv venv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: json_schema_utils
3
- Version: 0.8
3
+ Version: 0.8.2
4
4
  Summary: JSON Schema Utils
5
5
  Author: Fabien Coelho, Claire Yannou-Medrala
6
6
  License-Expression: CC0-1.0
@@ -28,7 +28,9 @@ Random utilities to analyze and manipulate JSON Schema.
28
28
 
29
29
  ## Installation
30
30
 
31
- Install latest version with `pip` from PyPI or from GitHub:
31
+ Install latest version with `pip` from
32
+ [PyPI](https://pypi.org/project/json-schema-utils) or directly from
33
+ [GitHub](https://github.com/zx80/json-schema-utils):
32
34
 
33
35
  ```sh
34
36
  python -m venv venv
@@ -84,7 +84,7 @@ def numberConstraints(schema):
84
84
  return constraints
85
85
 
86
86
 
87
- def buildModel(model, constraints: dict, defs: dict, sharp: dict, is_root = False):
87
+ def buildModel(model, constraints: dict, defs: dict, sharp: dict, is_root: bool = False):
88
88
  if constraints or sharp or defs:
89
89
 
90
90
  if constraints:
@@ -125,7 +125,7 @@ SPLITS = {
125
125
  "array": ["minItems", "maxItems", "uniqueItems", "items", "prefixItems", "contains",
126
126
  "minContains", "maxContains"],
127
127
  "object": ["properties", "required", "additionalProperties", "minProperties", "maxProperties",
128
- "patternProperties", "propertyNames"],
128
+ "patternProperties", "propertyNames"],
129
129
  }
130
130
 
131
131
  SPLIT = {}
@@ -197,7 +197,7 @@ def split_schema(schema: dict[str, Any]) -> dict[str, dict[str, Any]]:
197
197
 
198
198
  # identifiers
199
199
  CURRENT_SCHEMA: str|None = None
200
- SCHEMA: str|None = None
200
+ SCHEMA = None
201
201
  IDS: dict[str, dict[str, Any]] = {}
202
202
  EXPLICIT_TYPE: bool = False
203
203
 
@@ -251,7 +251,6 @@ def doubt(ok: bool, msg: str, strict: bool):
251
251
  else:
252
252
  log.warning(msg)
253
253
 
254
-
255
254
  def allOfLayer(schema: dict, operator: str):
256
255
  # log.warning(f"ao {operator} in: {schema}")
257
256
  schemas = copy.deepcopy(schema[operator])
@@ -273,7 +272,8 @@ def allOfLayer(schema: dict, operator: str):
273
272
 
274
273
 
275
274
  # TODO handle a global defs so as to be able to create new ones
276
- def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool = True):
275
+ def schema2model(schema, path: JsonPath = [],
276
+ strict: bool = True, fix: bool = True, is_root: bool = True):
277
277
  """Convert a JSON schema to a JSON model assuming a 2020-12 semantics."""
278
278
 
279
279
  global CURRENT_SCHEMA, SCHEMA
@@ -320,9 +320,9 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
320
320
  # keep json schema for handling $ref #
321
321
  IDS[dname][encoded] = val
322
322
  # provide a local converted version as well? not enough??
323
- defs[encoded] = schema2model(val, path + [dname, name], strict, False)
323
+ defs[encoded] = schema2model(val, path + [dname, name], strict, fix, False)
324
324
  # special root handling
325
- IDS[""] = "$#"
325
+ IDS[dname][""] = "$#"
326
326
 
327
327
  # FIXME cleanup OpenAPI extentions "x-*", nullable
328
328
  for prop in list(schema.keys()):
@@ -465,36 +465,36 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
465
465
  choices = schema["oneOf"]
466
466
  assert isinstance(choices, list), f"oneOf list at [{spath}]"
467
467
  if only(schema, "oneOf", *IGNORE):
468
- model = {"^": [schema2model(s, path + ["oneOf", i], strict, False)
469
- for i, s in enumerate(choices)]}
468
+ model = {"^": [schema2model(s, path + ["oneOf", i], strict, fix, False)
469
+ for i, s in enumerate(choices)]}
470
470
  return buildModel(model, {}, defs, sharp, is_root)
471
471
  else: # try building an "allOf" layer
472
472
  log.warning(f"keyword oneOf intermixed with other keywords at [{spath}]")
473
473
  ao = allOfLayer(schema, "oneOf")
474
- return schema2model(ao, path + ["oneOf"], strict, False)
474
+ return schema2model(ao, path + ["oneOf"], strict, fix, False)
475
475
  elif "anyOf" in schema:
476
476
  choices = schema["anyOf"]
477
477
  assert isinstance(choices, (list, tuple)), f"anyOf list at [{spath}]"
478
478
  if only(schema, "anyOf", *IGNORE):
479
- model = {"|": [schema2model(s, path + ["anyOf", i], strict, False)
479
+ model = {"|": [schema2model(s, path + ["anyOf", i], strict, fix, False)
480
480
  for i, s in enumerate(choices)]}
481
481
  return buildModel(model, {}, defs, sharp, is_root)
482
482
  else:
483
483
  log.warning(f"keyword anyOf intermixed with other keywords at [{spath}]")
484
484
  ao = allOfLayer(schema, "anyOf")
485
- return schema2model(ao, path + ["anyOf"], strict, False)
485
+ return schema2model(ao, path + ["anyOf"], strict, fix, False)
486
486
  elif "allOf" in schema:
487
487
  # NOTE types should be compatible to avoid an empty match
488
488
  choices = schema["allOf"]
489
489
  assert isinstance(choices, (list, tuple)), f"allOf list at [{spath}]"
490
490
  if only(schema, "allOf", *IGNORE):
491
- model = {"&": [schema2model(s, path + ["allOf", i], strict, False)
491
+ model = {"&": [schema2model(s, path + ["allOf", i], strict, fix, False)
492
492
  for i, s in enumerate(choices)]}
493
493
  return buildModel(model, {}, defs, sharp, is_root)
494
494
  else: # build another allOf layer
495
495
  log.warning(f"keyword allOf intermixed with other keywords at [{spath}]")
496
496
  ao = allOfLayer(schema, "allOf")
497
- return schema2model(ao, path + ["allOf"], strict, False)
497
+ return schema2model(ao, path + ["allOf"], strict, fix, False)
498
498
  elif "not" in schema:
499
499
  val = schema["not"]
500
500
  assert isinstance(val, dict), "not object at [{spath}]"
@@ -502,12 +502,12 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
502
502
  if len(val) == 0:
503
503
  model = "$NONE"
504
504
  else:
505
- model = {"^": ["$ANY", schema2model(val, path + ["not"], strict, False)]}
505
+ model = {"^": ["$ANY", schema2model(val, path + ["not"], strict, fix, False)]}
506
506
  return buildModel(model, {}, defs, sharp, is_root)
507
507
  else: # add a allOf layer
508
508
  log.warning(f"keyword not intermixed with other keywords at [{spath}]")
509
509
  ao = allOfLayer(schema, "not")
510
- return schema2model(ao, path + ["not"], strict, False)
510
+ return schema2model(ao, path + ["not"], strict, fix, False)
511
511
 
512
512
  # handle simpler schemas
513
513
  if "$ref" in schema:
@@ -534,7 +534,8 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
534
534
  if names and names[0] in ("$defs", "definitions"):
535
535
  val = IDS
536
536
  for name in names:
537
- assert name in val, f"following path in {ref}: missing {name} ({IDS}) at [{spath}]"
537
+ assert name in val, \
538
+ f"following path in {ref}: missing {name} ({IDS}) at [{spath}]"
538
539
  val = val[name]
539
540
  else: # arbitrary path
540
541
  val = SCHEMA
@@ -543,13 +544,13 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
543
544
  name = unquote(name).replace("~1", "/").replace("~0", "~")
544
545
  assert name in val, f"following path in {ref}: missing {name} at [{spath}]"
545
546
  val = val[name]
546
- model = schema2model(val, path + ["$ref"], strict, False)
547
+ model = schema2model(val, path + ["$ref"], strict, fix, False)
547
548
  return buildModel(model, {}, defs, sharp, is_root)
548
549
  else:
549
550
  assert False, f"$ref handling not implemented: {ref}"
550
551
  else:
551
552
  log.warning(f"$ref intermixed with other keywords at [{spath}]")
552
- return schema2model(allOfLayer(schema, "$ref"), path, strict, False)
553
+ return schema2model(allOfLayer(schema, "$ref"), path, strict, fix, False)
553
554
 
554
555
  elif "type" in schema:
555
556
  ts = schema["type"]
@@ -559,7 +560,8 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
559
560
  return buildModel(
560
561
  {
561
562
  "|": [
562
- schema2model(v, path + ["typeS"], strict, False) for v in schemas.values()
563
+ schema2model(v, path + ["typeS"], strict, fix, False)
564
+ for v in schemas.values()
563
565
  ]
564
566
  }, {}, defs, sharp, is_root)
565
567
  elif ts == "string" and "const" in schema:
@@ -618,7 +620,7 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
618
620
  elif ts == "number":
619
621
  doubt(only(schema, "type", "format", "multipleOf", "minimum", "maximum",
620
622
  "exclusiveMinimum", "exclusiveMaximum", *IGNORE),
621
- f"number properties at [{spath}]", strict)
623
+ f"number properties at [{spath}]", strict)
622
624
  model = "$NUMBER" if EXPLICIT_TYPE else -1.0
623
625
  if "format" in schema:
624
626
  fmt = schema["format"]
@@ -640,7 +642,7 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
640
642
  elif ts == "integer":
641
643
  doubt(only(schema, "type", "format", "multipleOf", "minimum", "maximum",
642
644
  "exclusiveMinimum", "exclusiveMaximum", *IGNORE),
643
- f"integer properties at [{spath}]", strict)
645
+ f"integer properties at [{spath}]", strict)
644
646
  model = "$INTEGER" if EXPLICIT_TYPE else -1
645
647
  # OpenAPI
646
648
  if "format" in schema:
@@ -668,23 +670,33 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
668
670
  return buildModel(model, {}, defs, sharp, is_root)
669
671
 
670
672
  elif ts == "array":
671
- # fix common misplacements
672
- if not strict and "items" in schema and isinstance(schema["items"], dict):
673
- subschema = schema["items"]
674
- for kw in ("uniqueItems", "minItems", "maxItems"):
675
- # possible move misplaced uniqueItems
676
- if kw not in schema and kw in subschema and "type" in subschema and \
677
- isinstance(subschema["type"], str) and subschema["type"] != "array":
678
- log.warning(f"moving misplaced {kw} at [{spath}.items]")
679
- schema[kw] = subschema[kw]
680
- del subschema[kw]
681
- # TODO minLength -> minItems
673
+ if fix:
674
+ # common misplacements
675
+ if not strict and "items" in schema and isinstance(schema["items"], dict):
676
+ subschema = schema["items"]
677
+ for kw in ("uniqueItems", "minItems", "maxItems"):
678
+ # possible move misplaced keyword
679
+ if kw not in schema and kw in subschema and "type" in subschema and \
680
+ isinstance(subschema["type"], str) and subschema["type"] != "array":
681
+ log.warning(f"moving misplaced {kw} at [{spath}.items]")
682
+ schema[kw] = subschema[kw]
683
+ del subschema[kw]
684
+ # common misnamings
685
+ rename = { "minLength": "minItems", "maxLength": "maxItems" }
686
+ for kw in rename:
687
+ if kw in schema:
688
+ if rename[kw] not in schema:
689
+ log.warning(f"renaming {kw} to {rename[kw]} at [{spath}]")
690
+ schema[rename[kw]] = schema[kw]
691
+ else:
692
+ log.warning(f"removing useless {kw} at [{spath}]")
693
+ del schema[kw]
682
694
 
683
695
  # sanity check
684
696
  doubt(only(schema, "type", "prefixItems", "items",
685
697
  "contains", "minContains", "maxContains",
686
698
  "minItems", "maxItems", "uniqueItems", *IGNORE),
687
- f"array properties at [{spath}]", strict)
699
+ f"array properties at [{spath}]", strict)
688
700
 
689
701
  # build model
690
702
  constraints = {}
@@ -715,7 +727,7 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
715
727
  vpi = schema["prefixItems"]
716
728
  assert isinstance(vpi, list), f"list prefixItems at [{spath}]"
717
729
  model = [
718
- schema2model(s, path + ["prefixItems", i], strict, False)
730
+ schema2model(s, path + ["prefixItems", i], strict, fix, False)
719
731
  for i, s in enumerate(vpi)
720
732
  ]
721
733
  if "items" in schema:
@@ -723,11 +735,13 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
723
735
  if isinstance(schema["items"], bool):
724
736
  assert not constraints, f"not implemented yet at [{spath}]"
725
737
  if not schema["items"]:
726
- return buildModel(model, {">=": 0, "<=": len(model)}, defs, sharp, is_root)
738
+ return buildModel(model, {">=": 0, "<=": len(model)},
739
+ defs, sharp, is_root)
727
740
  else:
728
741
  assert False, f"not implemented yet at [{spath}]"
729
742
  # items is a type
730
- model.append(schema2model(schema["items"], path + ["items"], strict, False))
743
+ model.append(schema2model(schema["items"], path + ["items"],
744
+ strict, fix, False))
731
745
  if not constraints:
732
746
  constraints[">="] = len(model) - 1
733
747
  return buildModel(model, constraints, defs, sharp, is_root)
@@ -743,7 +757,7 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
743
757
  if isinstance(sitems, list):
744
758
  # OLD JSON Schema prefixItems…
745
759
  array = [
746
- schema2model(s, path + ["items", i], strict, False)
760
+ schema2model(s, path + ["items", i], strict, fix, False)
747
761
  for i, s in enumerate(sitems)
748
762
  ]
749
763
  if strict:
@@ -753,7 +767,7 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
753
767
  return { "@": array, ">=": 0 }
754
768
  else:
755
769
  assert isinstance(sitems, (dict, bool)), f"valid schema at [{spath}]"
756
- model = [schema2model(schema["items"], path + ["items"], strict, False)]
770
+ model = [schema2model(schema["items"], path + ["items"], strict, fix, False)]
757
771
  return buildModel(model, constraints, defs, sharp, is_root)
758
772
  elif "contains" in schema:
759
773
  # NO contains/items mixing yet
@@ -762,7 +776,7 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
762
776
  # NOTE contains is not really supported in jm v2, or rather as an extension
763
777
  model = {
764
778
  "@": ["$ANY"],
765
- ".in": schema2model(schema["contains"], path + ["contains"], strict, False)
779
+ ".in": schema2model(schema["contains"], path + ["contains"], strict, fix, False)
766
780
  }
767
781
  if "minContains" in schema:
768
782
  mini = schema["minContains"]
@@ -783,14 +797,17 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
783
797
  log.warning(f"ignoring discriminator at [{spath}]")
784
798
  del schema["discriminator"]
785
799
 
786
- # fix additionalProperties misplacement
787
- if not strict and "properties" in schema and isinstance(schema["properties"], dict) \
788
- and "additionalProperties" not in schema \
789
- and "additionalProperties" in schema["properties"] \
790
- and "properties" not in schema["properties"]:
791
- log.warning(f"moving misplaced additionalProperties at [{spath}.properties]")
792
- schema["additionalProperties"] = schema["properties"]["additionalProperties"]
793
- del schema["properties"]["additionalProperties"]
800
+ if fix:
801
+ # common misplacements
802
+ if not strict and "properties" in schema and isinstance(schema["properties"], dict):
803
+ properties = schema["properties"]
804
+ for kw in ["additionalProperties", "unevaluatedProperties",
805
+ "minProperties", "maxProperties"]:
806
+ if kw in properties:
807
+ if kw not in schema:
808
+ log.warning(f"moving misplaced {kw} at [{spath}.properties]")
809
+ schema[kw] = properties[kw]
810
+ del properties[kw]
794
811
 
795
812
  # sanity check
796
813
  doubt(only(schema, "type", "properties", "additionalProperties", "required",
@@ -819,13 +836,13 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
819
836
  assert isinstance(pats, dict), f"dict pattern props at [{spath}]"
820
837
  for pp in sorted(pats.keys()):
821
838
  model[f"/{pp}/"] = \
822
- schema2model(pats[pp], path + ["patternProperties", pp], strict, False)
839
+ schema2model(pats[pp], path + ["patternProperties", pp], strict, fix, False)
823
840
 
824
841
  if "propertyNames" in schema:
825
842
  # does not seem very useful?
826
843
  pnames = schema["propertyNames"]
827
844
  if "additionalProperties" in schema:
828
- target = schema2model(schema["additionalProperties"], path, strict, False)
845
+ target = schema2model(schema["additionalProperties"], path, strict, fix, False)
829
846
  else:
830
847
  target = "$ANY"
831
848
 
@@ -852,10 +869,10 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
852
869
  for k, v in props.items():
853
870
  if k in required:
854
871
  model[f"_{k}"] = \
855
- schema2model(v, path + ["properties", f"_{k}"], strict, False)
872
+ schema2model(v, path + ["properties", f"_{k}"], strict, fix, False)
856
873
  else:
857
874
  model[f"?{k}"] = \
858
- schema2model(v, path + ["properties", f"?{k}"], strict, False)
875
+ schema2model(v, path + ["properties", f"?{k}"], strict, fix, False)
859
876
  if "additionalProperties" in schema:
860
877
  ap = schema["additionalProperties"]
861
878
  if isinstance(ap, bool):
@@ -863,7 +880,8 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
863
880
  model[""] = "$ANY"
864
881
  # else nothing else is allowed
865
882
  elif isinstance(ap, dict):
866
- model[""] = schema2model(ap, path + ["additionalProperties"], strict, False)
883
+ model[""] = schema2model(ap, path + ["additionalProperties"],
884
+ strict, fix, False)
867
885
  else:
868
886
  assert False, f"not implemented yet at [{spath}]"
869
887
  else:
@@ -873,7 +891,7 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
873
891
  # "additionalProperties" without "properties" or "patternProperties"
874
892
  doubt(only(schema, "type", "additionalProperties", "maxProperties", "minProperties",
875
893
  "properties", "patternProperties", *IGNORE),
876
- f"add prop props at [{spath}]", strict)
894
+ f"add prop props at [{spath}]", strict)
877
895
  if "properties" not in schema and "additionalProperties" not in schema and strict:
878
896
  assert False, "additionalProperties without properties or patternProperties"
879
897
  ap = schema["additionalProperties"]
@@ -882,7 +900,8 @@ def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool
882
900
  model[""] = "$ANY"
883
901
  # else nothing else is allowed
884
902
  elif isinstance(ap, dict):
885
- model[""] = schema2model(ap, path + ["additionalProperties"], strict, False)
903
+ model[""] = schema2model(ap, path + ["additionalProperties"],
904
+ strict, fix, False)
886
905
  else:
887
906
  assert False, f"not implemented yet at [{spath}]"
888
907
 
@@ -164,7 +164,7 @@ def jsu_check():
164
164
 
165
165
  # be nice
166
166
  if isinstance(jschema, dict) and "$schema" not in jschema:
167
- jschema["$schema"] = f"https://json-schema.org/draft/{args.version}/schema"
167
+ jschema["$schema"] = f"https://json-schema.org/draft/{args.draft}/schema"
168
168
 
169
169
  try:
170
170
  if args.engine == "jschon":
@@ -297,6 +297,7 @@ def jsu_pretty():
297
297
  schema = json.load(open(fn) if fn != "-" else sys.stdin)
298
298
  print(json_dumps(schema, args))
299
299
 
300
+
300
301
  GH = "https://raw.githubusercontent.com"
301
302
  SS = "https://json.schemastore.org"
302
303
  # JM = f"{GH}/clairey-zx81/json-model/main/models"
@@ -354,12 +355,15 @@ def jsu_model():
354
355
  from .convert import schema2model
355
356
 
356
357
  ap = argparse.ArgumentParser()
358
+ arg = ap.add_argument
357
359
  ap_common(ap)
358
- ap.add_argument("--id", action="store_true", default=False, help="enable $id lookup")
359
- ap.add_argument("--no-id", dest="id", action="store_false", help="disable $id lookup")
360
- ap.add_argument("--strict", action="store_true", default=True, help="reject doubtful schemas")
361
- ap.add_argument("--loose", dest="strict", action="store_false", help="accept doubtful schemas")
362
- ap.add_argument("schemas", nargs="*", help="schemas to process")
360
+ arg("--id", action="store_true", default=False, help="enable $id lookup")
361
+ arg("--no-id", dest="id", action="store_false", help="disable $id lookup")
362
+ arg("--strict", action="store_true", default=True, help="reject doubtful schemas")
363
+ arg("--loose", dest="strict", action="store_false", help="accept doubtful schemas")
364
+ arg("--fix", "-F", action="store_true", default=True, help="fix common schema issues")
365
+ arg("--no-fix", "-nF", dest="fix", action="store_false", help="do not fix common schema issues")
366
+ arg("schemas", nargs="*", help="schemas to process")
363
367
  args = ap.parse_args()
364
368
 
365
369
  log.setLevel(logging.DEBUG if args.debug else logging.WARNING if args.quiet else logging.INFO)
@@ -386,7 +390,7 @@ def jsu_model():
386
390
  log.info(f"using predefined model for {sid}")
387
391
  model = f"${ID2MODEL[sid][0 if args.strict else 1]}"
388
392
  if model is None:
389
- model = schema2model(schema, strict=args.strict)
393
+ model = schema2model(schema, strict=args.strict, fix=args.fix)
390
394
  except Exception as e:
391
395
  log.error(e, exc_info=args.debug)
392
396
  errors += 1
@@ -84,6 +84,7 @@ def _typeCompat(t: str, v: Any) -> bool:
84
84
  (t == "array" and isinstance(v, (list, tuple))) or
85
85
  (t == "object" and isinstance(v, dict)))
86
86
 
87
+
87
88
  _IGNORABLE = (
88
89
  # core
89
90
  "$schema", "$id", "$comment", "$vocabulary", "$anchor", "$dynamicAnchor",
@@ -131,6 +132,7 @@ def simplifySchema(schema: JsonSchema, url: str):
131
132
  # FIXME check that there is only one dynamicAnchor of this name?!
132
133
  dynroot: str|None = None
133
134
  if isinstance(schema, dict) and "$dynamicAnchor" in schema:
135
+ assert isinstance(schema["$dynamicAnchor"], str)
134
136
  dynroot = schema["$dynamicAnchor"]
135
137
  del schema["$dynamicAnchor"]
136
138
 
@@ -216,6 +218,7 @@ def simplifySchema(schema: JsonSchema, url: str):
216
218
  for kw in ("then", "else"):
217
219
  if kw in schema:
218
220
  subs = schema[kw]
221
+ assert isinstance(subs, dict)
219
222
  compat = True
220
223
  for k, v in subs.items():
221
224
  if k in _IGNORABLE:
@@ -225,6 +228,7 @@ def simplifySchema(schema: JsonSchema, url: str):
225
228
  elif k in schema:
226
229
  # special case, check for inclusion
227
230
  if k == "required":
231
+ assert "required" in schema and isinstance(schema["required"], list)
228
232
  assert isinstance(v, list) # and str
229
233
  for n in v:
230
234
  if n not in schema["required"]:
@@ -248,6 +252,7 @@ def simplifySchema(schema: JsonSchema, url: str):
248
252
  # simplify condition if possible
249
253
  if "if" in schema:
250
254
  cond = schema["if"]
255
+ assert isinstance(cond, dict)
251
256
  if "not" in cond and only(cond, "not", *_IGNORABLE):
252
257
  log.info("simplifying if not")
253
258
  schema["if"] = cond["not"]
@@ -340,11 +345,14 @@ def simplifySchema(schema: JsonSchema, url: str):
340
345
  if "propertyNames" in schema and "additionalProperties" in schema and \
341
346
  "properties" not in schema and "patternProperties" not in schema:
342
347
  pn = schema["propertyNames"]
348
+ assert isinstance(pn, dict)
343
349
  ap = schema["additionalProperties"]
344
350
  if "pattern" in pn and only(pn, "pattern", "type", *_IGNORABLE):
345
- log.info(f"switching propertyNames and additionalProperties to patternProperties at {lpath}")
351
+ log.info("switching propertyNames and additionalProperties to "
352
+ f"patternProperties at {lpath}")
346
353
  del schema["propertyNames"]
347
354
  del schema["additionalProperties"]
355
+ assert isinstance(pn["pattern"], str)
348
356
  schema["patternProperties"] = { pn["pattern"]: ap }
349
357
 
350
358
  # const/enum
@@ -370,11 +378,13 @@ def simplifySchema(schema: JsonSchema, url: str):
370
378
 
371
379
  return recurseSchema(schema, url, rwt=rwtSimpler)
372
380
 
381
+
373
382
  #
374
383
  # move definitions at the root and resolve ids
375
384
  #
376
385
  from urllib.parse import quote, unquote
377
386
 
387
+
378
388
  def _defId(schema) -> tuple[str|None, str|None]:
379
389
  """return name of definitions and id properties."""
380
390
  if not isinstance(schema, dict):
@@ -387,29 +397,38 @@ def _defId(schema) -> tuple[str|None, str|None]:
387
397
  None
388
398
  return (defn, idn)
389
399
 
400
+
390
401
  _SUBCOUNT: int = 0
391
402
 
392
403
  # TODO handle arbitrary path references
393
404
 
394
405
  def _scopeSubDefs(schema: JsonSchema, defs: dict[str, JsonSchema], rootdef: str,
395
- moved: dict[str, str], ids: dict[str, str], delete: list[tuple[Any, str]],
406
+ moved: dict[str, str], ids: dict[str, str],
407
+ delete: list[tuple[Any, str, str|None, str|None, str|None]],
396
408
  path: list[str|int] = []):
397
409
 
398
410
  log.debug(f"handing $ids/$defs at {path}")
399
411
 
400
412
  global _SUBCOUNT
401
413
  defn, idn = _defId(schema)
402
-
403
414
  if defn is None:
404
415
  return
405
416
 
417
+ assert isinstance(schema, dict)
418
+ assert isinstance(defn, str)
419
+ sdefs = schema[defn]
420
+ assert isinstance(sdefs, dict)
421
+
406
422
  if path and defn and not idn:
407
423
  # nested definitions, move them up
408
424
 
409
425
  prefix = f"_defs_{_SUBCOUNT}_"
410
426
  _SUBCOUNT += 1
411
427
 
412
- for name, sschema in schema[defn].items():
428
+ for name, sschema in sdefs.items():
429
+
430
+ assert isinstance(sschema, dict)
431
+
413
432
  # FIXME name may be quite ugly… eg a full URL
414
433
  if "/" not in name: # reuse name if simple
415
434
  new_name = prefix + name
@@ -419,7 +438,7 @@ def _scopeSubDefs(schema: JsonSchema, defs: dict[str, JsonSchema], rootdef: str,
419
438
  _SUBCOUNT += 1
420
439
  old_name = quote(name).replace("~", "~0").replace("/", "~1")
421
440
  npath = rootdef + "/" + new_name
422
- opath = f"#/{'/'.join(path)}/{defn}/{old_name}"
441
+ opath = f"#/{'/'.join(path)}/{defn}/{old_name}" # type: ignore
423
442
  sschema["$comment"] = f"origin: {opath}"
424
443
  moved[opath] = npath
425
444
  defs[new_name] = sschema
@@ -468,9 +487,10 @@ def _scopeSubDefs(schema: JsonSchema, defs: dict[str, JsonSchema], rootdef: str,
468
487
  recurseSchema(schema, "", rwt=rwtRef)
469
488
 
470
489
  # move local definitions as global
471
- for name, sschem in schema[defn].items():
490
+ for name, sschem in sdefs.items():
472
491
  pname = prefix + name
473
492
  assert pname not in defs
493
+ assert isinstance(sschem, (bool, dict))
474
494
  defs[pname] = sschem
475
495
 
476
496
  # we need to keep the schema in place for handling arbitrary url
@@ -499,6 +519,7 @@ def scopeDefs(schema: JsonSchema):
499
519
  return
500
520
 
501
521
  # ensure definitions root
522
+ assert isinstance(schema, dict)
502
523
  defn, idn = _defId(schema)
503
524
 
504
525
  if defn is None:
@@ -509,10 +530,10 @@ def scopeDefs(schema: JsonSchema):
509
530
  rootdef, moved, ids, delete = f"#/{defn}", {}, {}, []
510
531
 
511
532
  for s, p in todo_ids:
512
- _scopeSubDefs(s, schema[defn], rootdef, moved, ids, delete, p)
533
+ _scopeSubDefs(s, schema[defn], rootdef, moved, ids, delete, p) # type: ignore
513
534
 
514
535
  for s, p in todo_defs:
515
- _scopeSubDefs(s, schema[defn], rootdef, moved, ids, delete, p)
536
+ _scopeSubDefs(s, schema[defn], rootdef, moved, ids, delete, p) # type: ignore
516
537
 
517
538
  # move arbitrary references
518
539
  def mvRef(rschema, path):
@@ -529,7 +550,7 @@ def scopeDefs(schema: JsonSchema):
529
550
  # hmmm
530
551
  if segment in jdest:
531
552
  jdest = jdest[segment]
532
- elif "~" in segment or "%" in segment:
553
+ elif "~" in segment or "%" in segment:
533
554
  segment = unquote(segment).replace("~1", "/").replace("~0", "~")
534
555
  jdest = jdest[segment]
535
556
  elif isinstance(jdest, list):
@@ -541,7 +562,7 @@ def scopeDefs(schema: JsonSchema):
541
562
  _SUBCOUNT += 1
542
563
  ndest = f"#/{defn}/{name}"
543
564
  # log.info(f"moving {dest} to {ndest}")
544
- schema[defn][name] = copy.deepcopy(jdest)
565
+ schema[defn][name] = copy.deepcopy(jdest) # type: ignore
545
566
  rschema["$ref"] = ndest
546
567
  moved[dest] = ndest # for other identical references
547
568
  # TODO also rename ugly references?
@@ -574,7 +595,7 @@ def scopeDefs(schema: JsonSchema):
574
595
  del j[n]
575
596
  if prefix is not None:
576
597
  # move whole id-ed object as global as well, replaced with a ref
577
- schema[defn][prefix] = { p: s for p, s in j.items() }
598
+ schema[defn][prefix] = { p: s for p, s in j.items() } # type: ignore
578
599
  j.clear()
579
600
  j["$comment"] = f"{sid} moved as $def"
580
601
  j["$ref"] = dest
@@ -122,6 +122,8 @@ PER_TYPE = {
122
122
  "combi": [ "allOf", "anyOf", "oneOf", "if", "then", "else", "not" ],
123
123
  }
124
124
 
125
+ IGNORABLE = { kw for kw in PER_TYPE["meta"] }
126
+
125
127
  PROP_TO_TYPE: dict[str, str] = {}
126
128
  for t, props in PER_TYPE.items():
127
129
  for prop in props:
@@ -887,6 +889,15 @@ def _json_schema_stats_rec(
887
889
  elif "then" in jdata or "else" in jdata:
888
890
  collectErr(collection, "cond error", "then/else no if", path)
889
891
 
892
+ #
893
+ # $ref on draft 7 and prior tells to ignore all adjacent keywords!
894
+ #
895
+ # https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.3
896
+ # _All other properties in a "$ref" object MUST be ignored._
897
+ if "$ref" in jdata and 0 < collection["<version>"] <= 7:
898
+ if not set(jdata.keys()).issubset(IGNORABLE | {"$ref"}):
899
+ collectErr(collection, "ignore error", "adjacent keywords next to $ref", path)
900
+
890
901
  # scan all properties
891
902
  for prop, val in jdata.items():
892
903
 
@@ -8,7 +8,7 @@ exclude = ["tests*"]
8
8
 
9
9
  [project]
10
10
  name = "json_schema_utils"
11
- version = "0.8"
11
+ version = "0.8.2"
12
12
  authors = [ { name = "Fabien Coelho" }, { name = "Claire Yannou-Medrala" } ]
13
13
  description = "JSON Schema Utils"
14
14
  readme = "README.md"