diff --git a/python_jsonschema_objects/descriptors.py b/python_jsonschema_objects/descriptors.py index 3860307..421df26 100644 --- a/python_jsonschema_objects/descriptors.py +++ b/python_jsonschema_objects/descriptors.py @@ -80,7 +80,13 @@ def __set__(self, obj, val): break elif isinstance(typ, TypeProxy): try: - val = typ(**util.coerce_for_expansion(val)) + # handle keyword expansion according to expected types + # using keywords like oneOf, value can be an object, array or literal + val = util.coerce_for_expansion(val) + if isinstance(val, dict): + val = typ(**val) + else: + val = typ(val) val.validate() except Exception as e: errors.append("Failed to coerce to '{0}': {1}".format(typ, e)) @@ -119,7 +125,11 @@ def __set__(self, obj, val): val.validate() elif isinstance(info["type"], TypeProxy): - val = info["type"](val) + val = util.coerce_for_expansion(val) + if isinstance(val, dict): + val = info["type"](**val) + else: + val = info["type"](val) elif isinstance(info["type"], TypeRef): if not isinstance(val, info["type"].ref_class): diff --git a/python_jsonschema_objects/pattern_properties.py b/python_jsonschema_objects/pattern_properties.py index 3ca1d07..e4ccfea 100644 --- a/python_jsonschema_objects/pattern_properties.py +++ b/python_jsonschema_objects/pattern_properties.py @@ -71,6 +71,14 @@ def _make_type(self, typ, val): ): return typ(val) + if isinstance(typ, cb.TypeProxy): + val = util.coerce_for_expansion(val) + if isinstance(val, dict): + val = typ(**val) + else: + val = typ(val) + return val + raise validators.ValidationError( "additionalProperty type {0} was neither a literal " "nor a schema wrapper: {1}".format(typ, val) @@ -95,7 +103,7 @@ def instantiate(self, name, val): valtype = valtype[0] return MakeLiteral(name, valtype, val) - elif isinstance(self._additional_type, type): + elif isinstance(self._additional_type, (type, cb.TypeProxy)): return self._make_type(self._additional_type, val) raise validators.ValidationError( diff --git a/test/test_nested_arrays.py b/test/test_nested_arrays.py index a4aacb6..89f2f32 100644 --- a/test/test_nested_arrays.py +++ b/test/test_nested_arrays.py @@ -9,12 +9,14 @@ @pytest.fixture def nested_arrays(): return { + "$schema": "http://json-schema.org/draft-04/schema#", "title": "example", "properties": { "foo": { "type": "array", "items": { "type": "array", + # FIXME: not supported anymore in https://json-schema.org/draft/2020-12 "items": [{"type": "number"}, {"type": "number"}], }, } diff --git a/test/test_regression_232.py b/test/test_regression_232.py new file mode 100644 index 0000000..ca544d3 --- /dev/null +++ b/test/test_regression_232.py @@ -0,0 +1,129 @@ +import jsonschema +import pytest +import python_jsonschema_objects as pjo + +schema = { + "$schema": "http://json-schema.org/draft-07/schema", + "title": "myschema", + "type": "object", + "definitions": { + "RefObject": { + "title": "Ref Object", + "properties": {"location": {"$ref": "#/definitions/Location"}}, + }, + "MapObject": { + "title": "Map Object", + "additionalProperties": {"$ref": "#/definitions/Location"}, + }, + "MainObject": { + "title": "Main Object", + "additionalProperties": False, + "type": "object", + "properties": { + "location": { + "title": "location", + "oneOf": [ + {"$ref": "#/definitions/UNIQUE_STRING"}, + {"$ref": "#/definitions/Location"}, + ], + } + }, + }, + "Location": { + "title": "Location", + "description": "A Location represents a span on a specific sequence.", + "oneOf": [ + {"$ref": "#/definitions/LocationIdentifier"}, + {"$ref": "#/definitions/LocationTyped"}, + ], + }, + "LocationIdentifier": { + "type": "integer", + "minimum": 1, + }, + "LocationTyped": { + "additionalProperties": False, + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["Location"], + "default": "Location", + } + }, + }, + "UNIQUE_STRING": { + "additionalProperties": False, + "type": "string", + "pattern": "^\\w[^:]*:.+$", + }, + }, +} + + +@pytest.fixture +def schema_json(): + return schema + + +def test_nested_oneof_with_different_types(schema_json): + builder = pjo.ObjectBuilder(schema_json) + ns = builder.build_classes() + + resolver = jsonschema.RefResolver.from_schema(schema_json) + main_obj = schema_json["definitions"]["MainObject"] + + test1 = {"location": 12345} + test2 = {"location": {"type": "Location"}} + test3 = {"location": "unique:12"} + jsonschema.validate(test1, main_obj, resolver=resolver) + jsonschema.validate(test2, main_obj, resolver=resolver) + jsonschema.validate(test3, main_obj, resolver=resolver) + + obj1 = ns.MainObject(**test1) + obj2 = ns.MainObject(**test2) + obj3 = ns.MainObject(**test3) + + assert obj1.location == 12345 + assert obj2.location.type == "Location" + assert obj3.location == "unique:12" + + +def test_nested_oneof_with_different_types_by_reference(schema_json): + builder = pjo.ObjectBuilder(schema_json) + ns = builder.build_classes() + + resolver = jsonschema.RefResolver.from_schema(schema_json) + ref_obj = schema_json["definitions"]["RefObject"] + + test1 = {"location": 12345} + test2 = {"location": {"type": "Location"}} + jsonschema.validate(test1, ref_obj, resolver=resolver) + jsonschema.validate(test2, ref_obj, resolver=resolver) + + obj1 = ns.RefObject(**test1) + obj2 = ns.RefObject(**test2) + + assert obj1.location == 12345 + assert obj2.location.type == "Location" + + +def test_nested_oneof_with_different_types_in_additional_properties(schema_json): + builder = pjo.ObjectBuilder(schema_json) + ns = builder.build_classes() + + resolver = jsonschema.RefResolver.from_schema(schema_json) + map_obj = schema_json["definitions"]["MapObject"] + + x_prop_name = "location-id" + + test1 = {x_prop_name: 12345} + test2 = {x_prop_name: {"type": "Location"}} + jsonschema.validate(test1, map_obj, resolver=resolver) + jsonschema.validate(test2, map_obj, resolver=resolver) + + obj1 = ns.MapObject(**test1) + obj2 = ns.MapObject(**test2) + + assert obj1[x_prop_name] == 12345 + assert obj2[x_prop_name].type == "Location" diff --git a/tox.ini b/tox.ini index b42775b..8195127 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,10 @@ [tox] -envlist = py{35,36,37,38}-jsonschema{23,24,25,26,30}-markdown{2,3} +envlist = py{36,37,38}-jsonschema{23,24,25,26,30}-markdown{2,3} skip_missing_interpreters = true [gh-actions] python = - 3.5: py35 3.6: py36 3.7: py37 3.8: py38 @@ -13,8 +12,8 @@ python = [testenv] ;install_command = pip install {opts} {packages} -commands = coverage run {envbindir}/py.test --doctest-glob='python_jsonschema_objects/*.md' {posargs} - coverage xml --omit=*test* --include=*python_jsonschema_objects* +commands = python -m coverage run {envbindir}/py.test --doctest-glob='python_jsonschema_objects/*.md' {posargs} + python -m coverage xml --omit="*test*" deps = coverage pytest