From e4dd4f831a371a7b00ddbc78238b23445434a1a8 Mon Sep 17 00:00:00 2001 From: oyeong011 Date: Tue, 8 Sep 2026 10:10:49 +0900 Subject: [PATCH 1/2] Preserve boolean distinctions in nested JSON equality Nested lists and objects must compare their values using JMESPath equality instead of Python numeric coercion. Confidence: high Scope-risk: narrow Tested: 994 tests passed; nested-filter public API QA Not-tested: Python versions outside 3.12 --- jmespath/visitor.py | 6 ++++++ tests/test_search.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/jmespath/visitor.py b/jmespath/visitor.py index 15fb1774..f79957fc 100644 --- a/jmespath/visitor.py +++ b/jmespath/visitor.py @@ -8,6 +8,12 @@ def _equals(x, y): if _is_special_number_case(x, y): return False + elif isinstance(x, list) and isinstance(y, list): + return len(x) == len(y) and all( + _equals(a, b) for a, b in zip(x, y)) + elif isinstance(x, dict) and isinstance(y, dict): + return x.keys() == y.keys() and all( + _equals(x[key], y[key]) for key in x) else: return x == y diff --git a/tests/test_search.py b/tests/test_search.py index 4832079b..1428956b 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -62,3 +62,37 @@ def test_can_handle_decimals_as_numeric_type(self): result = decimal.Decimal('3') self.assertEqual(jmespath.search('[?a >= `1`].a', [{'a': result}]), [result]) + + +class TestNestedEquality(unittest.TestCase): + def test_nested_booleans_are_not_numbers(self): + for left, right in [ + ([True], [1]), + ([False], [0]), + ({'value': True}, {'value': 1}), + ({'value': [False]}, {'value': [0]}), + ([{'value': True}], [{'value': 1.0}])]: + for a, b in [(left, right), (right, left)]: + data = {'a': a, 'b': b} + self.assertFalse(jmespath.search('a == b', data)) + self.assertTrue(jmespath.search('a != b', data)) + + def test_nested_equality_preserves_json_semantics(self): + cases = [ + ([1], [1.0], True), + ({'a': [True, 1]}, {'a': [True, 1.0]}, True), + ({'a': 1, 'b': 2}, {'b': 2, 'a': 1}, True), + ([1], [1, 2], False), + ({'a': None}, {'b': None}, False), + ([], {}, False), + ([], [], True), + ({}, {}, True), + ] + for a, b, expected in cases: + self.assertEqual(jmespath.search('a == b', {'a': a, 'b': b}), + expected) + + def test_filter_rejects_nested_boolean_number_matches(self): + data = [{'value': [True]}, {'value': [1]}, {'value': [1.0]}] + self.assertEqual(jmespath.search('[?value == `[1]`]', data), + data[1:]) From 4e63e2229a45e6b0adf58dd97fd383837b096e7f Mon Sep 17 00:00:00 2001 From: oyeong011 Date: Tue, 8 Sep 2026 10:14:11 +0900 Subject: [PATCH 2/2] Avoid consuming recursion depth during structural equality Use an explicit work stack so separately parsed deeply nested JSON remains comparable while preserving boolean and number distinctions. Confidence: high Scope-risk: narrow Tested: 995 tests passed; depth-600 array/object API QA Not-tested: Python versions outside 3.12; non-JSON cycles --- jmespath/visitor.py | 28 ++++++++++++++++++---------- tests/test_search.py | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/jmespath/visitor.py b/jmespath/visitor.py index f79957fc..5404fda0 100644 --- a/jmespath/visitor.py +++ b/jmespath/visitor.py @@ -6,16 +6,24 @@ def _equals(x, y): - if _is_special_number_case(x, y): - return False - elif isinstance(x, list) and isinstance(y, list): - return len(x) == len(y) and all( - _equals(a, b) for a, b in zip(x, y)) - elif isinstance(x, dict) and isinstance(y, dict): - return x.keys() == y.keys() and all( - _equals(x[key], y[key]) for key in x) - else: - return x == y + pending = [(x, y)] + while pending: + x, y = pending.pop() + if _is_special_number_case(x, y): + return False + elif isinstance(x, list) and isinstance(y, list): + if len(x) != len(y): + return False + if x is not y: + pending.extend(zip(reversed(x), reversed(y))) + elif isinstance(x, dict) and isinstance(y, dict): + if x.keys() != y.keys(): + return False + if x is not y: + pending.extend((x[key], y[key]) for key in reversed(x)) + elif not (x == y): + return False + return True def _is_special_number_case(x, y): diff --git a/tests/test_search.py b/tests/test_search.py index 1428956b..fd3fa664 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,5 +1,6 @@ import sys import decimal +import json from tests import unittest, OrderedDict import jmespath @@ -96,3 +97,18 @@ def test_filter_rejects_nested_boolean_number_matches(self): data = [{'value': [True]}, {'value': [1]}, {'value': [1.0]}] self.assertEqual(jmespath.search('[?value == `[1]`]', data), data[1:]) + + + def test_deeply_nested_json_equality(self): + for opening, closing in [('[', ']'), ('{"value":', '}')]: + for left, right, expected in [ + ('1', '1.0', True), + ('true', '1', False), + ('1', '2', False), + ('[]', '{}', False)]: + a = json.loads(opening * 600 + left + closing * 600) + b = json.loads(opening * 600 + right + closing * 600) + self.assertIsNot(a, b) + data = {'a': a, 'b': b} + self.assertEqual(jmespath.search('a == b', data), expected) + self.assertEqual(jmespath.search('a != b', data), not expected)