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.
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/Makefile +1 -1
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/PKG-INFO +4 -2
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/README.md +3 -1
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/PKG-INFO +4 -2
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/convert.py +74 -55
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/scripts.py +11 -7
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/simplify.py +32 -11
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/stats.py +11 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/pyproject.toml +1 -1
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/.gitignore +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/LICENSE +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/MANIFEST.in +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/SOURCES.txt +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/dependency_links.txt +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/entry_points.txt +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/requires.txt +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/top_level.txt +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/__init__.py +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/inline.py +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/recurse.py +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/schemas.py +0 -0
- {json_schema_utils-0.8 → json_schema_utils-0.8.2}/jsutils/utils.py +0 -0
- {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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 = [],
|
|
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
|
-
|
|
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,
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
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)},
|
|
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"],
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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"],
|
|
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
|
-
|
|
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"],
|
|
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.
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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(
|
|
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],
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{json_schema_utils-0.8 → json_schema_utils-0.8.2}/json_schema_utils.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|