diff --git a/CHANGELOG.txt b/CHANGELOG.txt index dd8f8fd..7a46120 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,7 @@ === UNRELEASED * Fixed `$` in `patternProperties` matching before a trailing newline, which also made a `pattern` with the same regex accept a trailing newline +* Added support of dependentRequired and dependentSchemas from draft-2019 === 2.22.3 (2026-11-01) diff --git a/fastjsonschema/draft04.py b/fastjsonschema/draft04.py index cba1e48..bc91f3b 100644 --- a/fastjsonschema/draft04.py +++ b/fastjsonschema/draft04.py @@ -664,20 +664,23 @@ def generate_dependencies(self): Since draft 06 definition can be boolean or empty array. True and empty array means nothing, False means that key cannot be there at all. """ + self._generate_dependencies('dependencies') + + def _generate_dependencies(self, rule): self.create_variable_is_dict() with self.l('if {variable}_is_dict:'): is_empty = True - for key, values in self._definition["dependencies"].items(): + for key, values in self._definition[rule].items(): if values == [] or values is True: continue is_empty = False with self.l('if "{}" in {variable}:', self.e(key)): if values is False: - self.exc('{} in {name} must not be there', self.e(key), rule='dependencies') + self.exc('{} in {name} must not be there', self.e(key), rule=rule) elif isinstance(values, list): for value in values: with self.l('if "{}" not in {variable}:', self.e(value)): - self.exc('{name} missing dependency {} for {}', self.e(value), self.e(key), rule='dependencies') + self.exc('{name} missing dependency {} for {}', self.e(value), self.e(key), rule=rule) else: code_len = len(self._code) self.generate_func_code_block(values, self._variable, self._variable_name, clear_variables=True) diff --git a/fastjsonschema/draft2019.py b/fastjsonschema/draft2019.py index 85c9c12..5f9063e 100644 --- a/fastjsonschema/draft2019.py +++ b/fastjsonschema/draft2019.py @@ -1,3 +1,5 @@ +from collections import OrderedDict + from .draft07 import CodeGeneratorDraft07 @@ -13,3 +15,55 @@ class CodeGeneratorDraft2019(CodeGeneratorDraft07): r')\Z' ), }) + + def __init__( + self, + definition, + resolver=None, + formats={}, + use_default=True, + use_formats=True, + detailed_exceptions=True, + fast_fail=True + ): + super().__init__(definition, resolver, formats, use_default, use_formats, detailed_exceptions, fast_fail) + # Like dependencies, check them before properties generates default values. + keywords = list(self._json_keywords_to_function.items()) + index = [key for key, _ in keywords].index('dependencies') + 1 + keywords[index:index] = [ + ('dependentRequired', self.generate_dependent_required), + ('dependentSchemas', self.generate_dependent_schemas), + ] + self._json_keywords_to_function = OrderedDict(keywords) + + def generate_dependent_required(self): + """ + Means when object has property, it needs to have also other properties. + + .. code-block:: python + + { + 'dependentRequired': { + 'bar': ['foo'], + }, + } + + Valid object is containing only foo, both bar and foo or none of them, but not + object with only bar. + """ + self._generate_dependencies('dependentRequired') + + def generate_dependent_schemas(self): + """ + Means when object has property, the whole object needs to be valid against + the schema. + + .. code-block:: python + + { + 'dependentSchemas': { + 'bar': {'required': ['foo']}, + }, + } + """ + self._generate_dependencies('dependentSchemas') diff --git a/tests/json_schema/test_draft2019.py b/tests/json_schema/test_draft2019.py index bf3e1b8..625f87a 100644 --- a/tests/json_schema/test_draft2019.py +++ b/tests/json_schema/test_draft2019.py @@ -31,8 +31,6 @@ def pytest_generate_tests(metafunc): 'anchor.json', 'content.json', 'defs.json', - 'dependentRequired.json', - 'dependentSchemas.json', 'maxContains.json', 'minContains.json', 'recursiveRef.json', diff --git a/tests/test_object.py b/tests/test_object.py index b683f61..d82ac5f 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -316,3 +316,19 @@ def test_dependencies_annotation_only_schema(): validator = fastjsonschema.compile(schema) assert validator({'foo': 1}) == {'foo': 1} assert validator({'bar': 1}) == {'bar': 1} + + +@pytest.mark.parametrize('keyword, definition', [ + ('dependentRequired', {'foo': ['bar']}), + ('dependentSchemas', {'foo': False}), +]) +def test_dependent_keywords_checked_before_defaults(keyword, definition): + validator = fastjsonschema.compile({ + '$schema': 'https://json-schema.org/draft/2019-09/schema', + 'properties': {'foo': {'default': 1}}, + keyword: definition, + }) + assert validator({}) == {'foo': 1} + with pytest.raises(JsonSchemaValueException) as exc: + validator({'foo': 1}) + assert exc.value.rule == keyword