diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ad5577c..1ed9a0a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -5,7 +5,7 @@ * Fixed comparing values for enum and const validations * Fixed resolving plain-name ref fragments * Fixed not throwing when unknown format is used -* Fixed compilation of if/then/else and propertyNames when if schema generates no validation code +* Fixed compilation of not, if/then/else, items, additionalItems, propertyNames and schema dependencies when a subschema generates no validation code * Added option to get all the errors (set `fast_fail` to `False`) * Added basic type hints * Added support of duration and uuid formats from draft-2019 diff --git a/fastjsonschema/draft04.py b/fastjsonschema/draft04.py index 93194d3..decb8bf 100644 --- a/fastjsonschema/draft04.py +++ b/fastjsonschema/draft04.py @@ -256,7 +256,10 @@ def generate_not(self): self.exc('{name} must NOT match a disallowed definition', rule='not') else: with self.l('try:', optimize=False): + code_len = len(self._code) self.generate_func_code_block(not_definition, self._variable, self._variable_name) + if len(self._code) == code_len: + self.l('pass') self.l('except JsonSchemaValueException: pass') with self.l('else:'): self.exc('{name} must NOT match a disallowed definition', rule='not') @@ -464,22 +467,24 @@ def generate_items(self): self.exc('{name} must contain only specified items', rule='items') else: with self.l('for {variable}_x, {variable}_item in enumerate({variable}[{0}:], {0}):', len(items_definition)): - count = self.generate_func_code_block( + code_len = len(self._code) + self.generate_func_code_block( self._definition['additionalItems'], '{}_item'.format(self._variable), '{}[{{{}_x}}]'.format(self._variable_name, self._variable), ) - if not count: + if len(self._code) == code_len: self.l('pass') else: if items_definition: with self.l('for {variable}_x, {variable}_item in enumerate({variable}):'): - count = self.generate_func_code_block( + code_len = len(self._code) + self.generate_func_code_block( items_definition, '{}_item'.format(self._variable), '{}[{{{}_x}}]'.format(self._variable_name, self._variable), ) - if not count: + if len(self._code) == code_len: self.l('pass') def generate_min_properties(self): @@ -658,6 +663,9 @@ def generate_dependencies(self): with self.l('if "{}" not in {variable}:', self.e(value)): self.exc('{name} missing dependency {} for {}', self.e(value), self.e(key), rule='dependencies') else: + code_len = len(self._code) self.generate_func_code_block(values, self._variable, self._variable_name, clear_variables=True) + if len(self._code) == code_len: + self.l('pass') if is_empty: self.l('pass') diff --git a/fastjsonschema/draft06.py b/fastjsonschema/draft06.py index 4a357a4..1e71f0f 100644 --- a/fastjsonschema/draft06.py +++ b/fastjsonschema/draft06.py @@ -126,13 +126,14 @@ def generate_property_names(self): self.l('{variable}_property_names = True') with self.l('for {variable}_key in {variable}:'): with self.l('try:'): - count = self.generate_func_code_block( + code_len = len(self._code) + self.generate_func_code_block( property_names_definition, '{}_key'.format(self._variable), self._variable_name, clear_variables=True, ) - if not count: + if len(self._code) == code_len: self.l('pass') with self.l('except JsonSchemaValueException:'): self.l('{variable}_property_names = False') diff --git a/fastjsonschema/draft07.py b/fastjsonschema/draft07.py index 985b4f5..685956e 100644 --- a/fastjsonschema/draft07.py +++ b/fastjsonschema/draft07.py @@ -68,22 +68,28 @@ def generate_if_then_else(self): self.l('pass') with self.l('except JsonSchemaValueException:'): if 'else' in self._definition: + code_len = len(self._code) self.generate_func_code_block( self._definition['else'], self._variable, self._variable_name, clear_variables=True ) + if len(self._code) == code_len: + self.l('pass') else: self.l('pass') if 'then' in self._definition: with self.l('else:'): + code_len = len(self._code) self.generate_func_code_block( self._definition['then'], self._variable, self._variable_name, clear_variables=True ) + if len(self._code) == code_len: + self.l('pass') def generate_content_encoding(self): """ diff --git a/tests/test_array.py b/tests/test_array.py index 0528228..20f7efd 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -207,3 +207,14 @@ def test_issue_114(asserter): value = {"a": []} expected = value asserter(schema, value, expected) + + +def test_items_subschema_without_validation_code(asserter): + asserter({'items': {'not': False}}, [1, 2], [1, 2]) + + +def test_additional_items_subschema_without_validation_code(asserter): + asserter({ + 'items': [{'type': 'string'}], + 'additionalItems': {'format': 'unknown-format'}, + }, ['a', 1], ['a', 1]) diff --git a/tests/test_common.py b/tests/test_common.py index a3268fc..29a3b13 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -99,3 +99,35 @@ def test_one_of_factorized(asserter, value, expected): ]) def test_not(asserter, value, expected): asserter({'not': {'type': 'number'}}, value, expected) + + +exc = JsonSchemaValueException('data must NOT match a disallowed definition', value='{data}', name='data', definition='{definition}', rule='not') +@pytest.mark.parametrize('subdefinition', [ + {'title': 'x'}, + {'description': 'd'}, + {'$comment': 'c'}, + {'examples': [1]}, + {'default': 5}, + {'readOnly': True}, + {'not': False}, + {'format': 'unknown-format'}, + {'uniqueItems': False}, +]) +@pytest.mark.parametrize('value', [0, 'abc', {}]) +def test_not_subschema_without_validation_code(asserter, subdefinition, value): + """Subschema generating no validation code matches everything, so not must reject everything.""" + asserter({'not': subdefinition}, value, exc) + + +@pytest.mark.parametrize('schema_version', [ + 'http://json-schema.org/draft-04/schema', + 'http://json-schema.org/draft-06/schema', + 'http://json-schema.org/draft-07/schema', + 'https://json-schema.org/draft/2019-09/schema', +]) +def test_not_annotation_only_all_drafts(asserter, schema_version): + asserter({'$schema': schema_version, 'not': {'title': 'x'}}, 1, exc) + + +def test_not_not_annotation_only(asserter): + asserter({'not': {'not': {'title': 'x'}}}, 1, 1) diff --git a/tests/test_composition.py b/tests/test_composition.py index b3e995c..cbd1eb8 100644 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -140,3 +140,31 @@ def test_if_then_empty_if(): "version": "2.0", "terminal": "t1", } + + +def test_if_then_else_annotation_only_then(): + schema = { + "$schema": "http://json-schema.org/draft-07/schema", + "if": {"type": "string"}, + "then": {"title": "x"}, + "else": {"type": "integer"}, + } + validator = fastjsonschema.compile(schema) + assert validator("abc") == "abc" + assert validator(42) == 42 + with pytest.raises(JsonSchemaValueException): + validator(4.5) + + +def test_if_then_else_annotation_only_else(): + schema = { + "$schema": "http://json-schema.org/draft-07/schema", + "if": {"type": "string"}, + "then": {"minLength": 2}, + "else": {"title": "x"}, + } + validator = fastjsonschema.compile(schema) + assert validator("abc") == "abc" + assert validator(42) == 42 + with pytest.raises(JsonSchemaValueException): + validator("a") diff --git a/tests/test_object.py b/tests/test_object.py index 575667b..b683f61 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -300,3 +300,19 @@ def test_property_names_description_only(): compile(code, '', 'exec') validator = fastjsonschema.compile(schema) assert validator({'build': {}, 'test': 1}) == {'build': {}, 'test': 1} + + +def test_property_names_subschema_without_validation_code(): + schema = { + 'type': 'object', + 'propertyNames': {'not': False}, + } + validator = fastjsonschema.compile(schema) + assert validator({'build': {}, 'test': 1}) == {'build': {}, 'test': 1} + + +def test_dependencies_annotation_only_schema(): + schema = {'dependencies': {'foo': {'title': 'x'}}} + validator = fastjsonschema.compile(schema) + assert validator({'foo': 1}) == {'foo': 1} + assert validator({'bar': 1}) == {'bar': 1}