From 10e952b40871b335896c1be1fb8444894fd6feb8 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 20 Jul 2026 11:28:47 -0400 Subject: [PATCH] fix: don't leak variables from not blocks into the outer scope Variables created while generating a `not` subschema stayed in the generator's variable-tracking set, so outer keywords like `additionalProperties` reused them instead of emitting their own assignments. Since the `not` block only succeeds by raising partway through, validating a passing document raised UnboundLocalError. Fixes #208 Assisted-by: ClaudeCode:claude-fable-5 --- fastjsonschema/draft04.py | 2 +- tests/test_common.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/fastjsonschema/draft04.py b/fastjsonschema/draft04.py index 93194d3..8533f6a 100644 --- a/fastjsonschema/draft04.py +++ b/fastjsonschema/draft04.py @@ -256,7 +256,7 @@ def generate_not(self): self.exc('{name} must NOT match a disallowed definition', rule='not') else: with self.l('try:', optimize=False): - self.generate_func_code_block(not_definition, self._variable, self._variable_name) + self.generate_func_code_block(not_definition, self._variable, self._variable_name, clear_variables=True) self.l('except JsonSchemaValueException: pass') with self.l('else:'): self.exc('{name} must NOT match a disallowed definition', rule='not') diff --git a/tests/test_common.py b/tests/test_common.py index a3268fc..503d5fa 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -99,3 +99,19 @@ def test_one_of_factorized(asserter, value, expected): ]) def test_not(asserter, value, expected): asserter({'not': {'type': 'number'}}, value, expected) + + +@pytest.mark.parametrize('value, expected', [ + ({'a': 1}, {'a': 1}), + ({'b': 2}, {'b': 2}), + ({'a': 1, 'b': 2}, JsonSchemaValueException('data must NOT match a disallowed definition', value='{data}', name='data', definition='{definition}', rule='not')), +]) +def test_not_with_object_and_additional_properties(asserter, value, expected): + # Regression test: variables created inside the not block must not leak + # into the outer scope (issue #208). + asserter({ + 'type': 'object', + 'properties': {'a': {}, 'b': {}}, + 'additionalProperties': False, + 'not': {'properties': {'a': {}, 'b': {}}, 'required': ['a', 'b']}, + }, value, expected)