From aa322b62cff5d07845079a5c92fef2431418a999 Mon Sep 17 00:00:00 2001 From: Mayank Dhingra Date: Fri, 23 Oct 2026 11:00:08 -0400 Subject: [PATCH] Preserve exact integer arithmetic for multipleOf Use integer remainders when both operands are integers. Floating-point quotients can round nonmultiples into integers or overflow on valid large multiples. Keep the existing float and Decimal paths unchanged. Test compiled and generated validators across supported schema drafts, including both error collection modes and unchanged noninteger operands. Signed-off-by: Mayank Dhingra --- fastjsonschema/draft04.py | 27 ++++++++------ tests/test_number.py | 76 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/fastjsonschema/draft04.py b/fastjsonschema/draft04.py index d86c592..679ca1b 100644 --- a/fastjsonschema/draft04.py +++ b/fastjsonschema/draft04.py @@ -361,17 +361,22 @@ def generate_multiple_of(self): with self.l('if isinstance({variable}, (int, float, Decimal)) and not isinstance({variable}, bool):'): if not isinstance(self._definition['multipleOf'], (int, float, decimal.Decimal)): raise JsonSchemaDefinitionException('multipleOf must be a number') - # For proper multiplication check of floats we need to use decimals, - # because for example 19.01 / 0.01 = 1901.0000000000002. - if isinstance(self._definition['multipleOf'], float): - self.l('quotient = Decimal(repr({variable})) / Decimal(repr({multipleOf}))') - else: - self.l('quotient = {variable} / {multipleOf}') - with self.l('if int(quotient) != quotient:'): - self.exc('{name} must be multiple of {multipleOf}', rule='multipleOf') - # For example, 1e308 / 0.123456789 - with self.l('if {variable} / {multipleOf} == float("inf"):'): - self.exc('inifinity reached', rule='multipleOf') + # Integer division through a float loses precision or can overflow. + with self.l('if isinstance({variable}, int) and isinstance({multipleOf}, int):'): + with self.l('if {variable} % {multipleOf} != 0:'): + self.exc('{name} must be multiple of {multipleOf}', rule='multipleOf') + with self.l('else:'): + # For proper multiplication check of floats we need to use decimals, + # because for example 19.01 / 0.01 = 1901.0000000000002. + if isinstance(self._definition['multipleOf'], float): + self.l('quotient = Decimal(repr({variable})) / Decimal(repr({multipleOf}))') + else: + self.l('quotient = {variable} / {multipleOf}') + with self.l('if int(quotient) != quotient:'): + self.exc('{name} must be multiple of {multipleOf}', rule='multipleOf') + # For example, 1e308 / 0.123456789 + with self.l('if {variable} / {multipleOf} == float("inf"):'): + self.exc('inifinity reached', rule='multipleOf') def generate_min_items(self): self.create_variable_is_list() diff --git a/tests/test_number.py b/tests/test_number.py index add0228..ced77fa 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -2,7 +2,7 @@ import pytest -from fastjsonschema import JsonSchemaValueException +from fastjsonschema import JsonSchemaValueException, JsonSchemaValuesException, compile, compile_to_code @pytest.fixture(params=['number', 'integer']) @@ -113,6 +113,80 @@ def test_multiple_of(asserter, number_type, value, expected): }, value, expected) +@pytest.fixture(params=[compile, compile_to_code], ids=['compiled', 'generated']) +def multiple_of_compiler(request): + def generate(definition, fast_fail): + validator = request.param(definition, fast_fail=fast_fail) + if isinstance(validator, str): + namespace = {} + exec(validator, namespace) + return namespace['validate'] + return validator + return generate + + +@pytest.mark.parametrize('schema_version', ['04', '06', '07']) +@pytest.mark.parametrize('fast_fail', [True, False]) +@pytest.mark.parametrize('divisor, value, valid', [ + (2, 2 ** 53 + 1, False), + (2, -(2 ** 53 + 1), False), + (2, 2 ** 63 - 1, False), + (2, 2 ** 63 - 2, True), + (3, 3 * 10 ** 400, True), + (3, -3 * 10 ** 400, True), + (3, 3 * 10 ** 400 + 1, False), + (3, -(3 * 10 ** 400 + 1), False), + (10 ** 400 + 3, 2 * (10 ** 400 + 3), True), + (10 ** 400 + 3, 2 * (10 ** 400 + 3) + 1, False), + (3, 0, True), + (1, 10 ** 400, True), +], ids=[ + 'above-float-precision', 'negative-above-float-precision', + 'signed-64-bit-odd', 'signed-64-bit-even', + 'huge-multiple', 'negative-huge-multiple', + 'huge-nonmultiple', 'negative-huge-nonmultiple', + 'huge-divisor-multiple', 'huge-divisor-nonmultiple', 'zero', 'unit-divisor', +]) +def test_multiple_of_large_integers(multiple_of_compiler, schema_version, fast_fail, divisor, value, valid): + definition = { + '$schema': 'http://json-schema.org/draft-{}/schema'.format(schema_version), + 'type': 'integer', + 'multipleOf': divisor, + } + validator = multiple_of_compiler(definition, fast_fail) + if valid: + assert validator(value) == value + else: + exception = JsonSchemaValueException if fast_fail else JsonSchemaValuesException + with pytest.raises(exception) as exc: + validator(value) + errors = [exc.value] if fast_fail else exc.value.errors + assert len(errors) == 1 + assert errors[0].rule == 'multipleOf' + assert errors[0].value == value + assert errors[0].name == 'data' + assert errors[0].definition == definition + + +@pytest.mark.parametrize('divisor, value, valid', [ + (3, 6.0, True), + (3, 4.5, False), + (3, decimal.Decimal('6'), True), + (3, decimal.Decimal('4.5'), False), + (0.01, 19.01, True), + (0.01, 19.001, False), + (decimal.Decimal('3'), 6, True), + (decimal.Decimal('3'), 4, False), +]) +def test_multiple_of_non_integer_operands(multiple_of_compiler, divisor, value, valid): + validator = multiple_of_compiler({'multipleOf': divisor}, True) + if valid: + assert validator(value) == value + else: + with pytest.raises(JsonSchemaValueException): + validator(value) + + exc = JsonSchemaValueException('data must be multiple of 0.0001', value='{data}', name='data', definition='{definition}', rule='multipleOf') @pytest.mark.parametrize('value, expected', [ (0.00751, exc),