From 071222cf1305677fff70886bbf92347a4cc17e87 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Wed, 22 Apr 2026 18:34:34 +0800 Subject: [PATCH 1/2] fix: multipleOf float precision with tolerance-based check The previous check used math.modf to verify that the quotient of value/multipleOf is an integer. Due to IEEE 754 floating-point precision, this fails for valid cases like 1.13 / 0.01 which yields 112.99999999999999 instead of 113. Replace with a tolerance-based check: round the quotient and verify the difference is within 1e-10. --- lib/jsonschema.lua | 6 +++-- spec/extra/multipleOf.json | 55 ++++++++++++++++++++++++++++++++++++++ t/draft4.lua | 1 + t/draft7.lua | 1 + 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 spec/extra/multipleOf.json diff --git a/lib/jsonschema.lua b/lib/jsonschema.lua index 1f69b18..ee63b11 100644 --- a/lib/jsonschema.lua +++ b/lib/jsonschema.lua @@ -984,9 +984,11 @@ generate_validator = function(ctx, schema) -- integer multipleOf: modulo is enough ctx:stmt(sformat(' if %s %% %d ~= 0 then', ctx:param(1), mof)) else - -- float multipleOf: it's a bit more hacky and slow + -- float multipleOf: use tolerance to handle IEEE 754 precision errors + -- e.g. 1.13 / 0.01 = 112.99999999999999 in Lua/LuaJIT ctx:stmt(sformat(' local quotient = %s / %s', ctx:param(1), mof)) - ctx:stmt(sformat(' if %s(quotient) ~= quotient then', ctx:libfunc('math.modf'))) + ctx:stmt(sformat(' local rounded = %s(quotient + 0.5)', ctx:libfunc('math.floor'))) + ctx:stmt(sformat(' if %s(quotient - rounded) > 1e-10 then', ctx:libfunc('math.abs'))) end ctx:stmt(sformat( ' return false, %s("expected %%s to be a multiple of %s", %s)', ctx:libfunc('string.format'), mof, ctx:param(1))) diff --git a/spec/extra/multipleOf.json b/spec/extra/multipleOf.json new file mode 100644 index 0000000..b2def79 --- /dev/null +++ b/spec/extra/multipleOf.json @@ -0,0 +1,55 @@ +[ + { + "description": "multipleOf with float precision", + "schema": { + "type": "number", + "multipleOf": 0.01 + }, + "tests": [ + { + "description": "1.13 is a multiple of 0.01", + "data": 1.13, + "valid": true + }, + { + "description": "0.01 is a multiple of 0.01", + "data": 0.01, + "valid": true + }, + { + "description": "100.05 is a multiple of 0.01", + "data": 100.05, + "valid": true + }, + { + "description": "1.1312 is not a multiple of 0.01", + "data": 1.1312, + "valid": false + }, + { + "description": "0.015 is not a multiple of 0.01", + "data": 0.015, + "valid": false + } + ] + }, + { + "description": "multipleOf with integer", + "schema": { + "type": "number", + "multipleOf": 3 + }, + "tests": [ + { + "description": "9 is a multiple of 3", + "data": 9, + "valid": true + }, + { + "description": "10 is not a multiple of 3", + "data": 10, + "valid": false + } + ] + } +] diff --git a/t/draft4.lua b/t/draft4.lua index 90004f8..3b392cd 100644 --- a/t/draft4.lua +++ b/t/draft4.lua @@ -47,6 +47,7 @@ local supported = { "spec/extra/dependencies.json", "spec/extra/table.json", "spec/extra/ref.json", + "spec/extra/multipleOf.json", 'spec/JSON-Schema-Test-Suite/tests/draft4/type.json', 'spec/JSON-Schema-Test-Suite/tests/draft4/default.json', diff --git a/t/draft7.lua b/t/draft7.lua index 8677c23..8425773 100644 --- a/t/draft7.lua +++ b/t/draft7.lua @@ -50,6 +50,7 @@ local supported = { "spec/extra/ref.json", "spec/extra/format.json", "spec/extra/default.json", + "spec/extra/multipleOf.json", 'spec/JSON-Schema-Test-Suite/tests/draft7/type.json', 'spec/JSON-Schema-Test-Suite/tests/draft7/default.json', From 0c41dff92489cb5eaa96f13226300c5280e432b7 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Wed, 22 Apr 2026 19:25:36 +0800 Subject: [PATCH 2/2] fix: use relative tolerance for multipleOf to handle large values Use 1e-12 relative tolerance scaled by the rounded quotient magnitude, preventing false positives with large multipleOf values (e.g. multipleOf=1e9). Add test case for large multipleOf values as suggested. --- lib/jsonschema.lua | 9 ++++++--- spec/extra/multipleOf.json | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/jsonschema.lua b/lib/jsonschema.lua index ee63b11..54e82e3 100644 --- a/lib/jsonschema.lua +++ b/lib/jsonschema.lua @@ -984,11 +984,14 @@ generate_validator = function(ctx, schema) -- integer multipleOf: modulo is enough ctx:stmt(sformat(' if %s %% %d ~= 0 then', ctx:param(1), mof)) else - -- float multipleOf: use tolerance to handle IEEE 754 precision errors - -- e.g. 1.13 / 0.01 = 112.99999999999999 in Lua/LuaJIT + -- float multipleOf: use relative tolerance to handle IEEE 754 + -- precision errors. e.g. 1.13 / 0.01 = 112.99999999999999 + -- We check whether the fractional part of the quotient is + -- negligible relative to its magnitude. ctx:stmt(sformat(' local quotient = %s / %s', ctx:param(1), mof)) ctx:stmt(sformat(' local rounded = %s(quotient + 0.5)', ctx:libfunc('math.floor'))) - ctx:stmt(sformat(' if %s(quotient - rounded) > 1e-10 then', ctx:libfunc('math.abs'))) + ctx:stmt( ' local tol = 1e-12 * (rounded == 0 and 1 or (rounded < 0 and -rounded or rounded))') + ctx:stmt(sformat(' if %s(quotient - rounded) > tol then', ctx:libfunc('math.abs'))) end ctx:stmt(sformat( ' return false, %s("expected %%s to be a multiple of %s", %s)', ctx:libfunc('string.format'), mof, ctx:param(1))) diff --git a/spec/extra/multipleOf.json b/spec/extra/multipleOf.json index b2def79..22c6880 100644 --- a/spec/extra/multipleOf.json +++ b/spec/extra/multipleOf.json @@ -51,5 +51,24 @@ "valid": false } ] + }, + { + "description": "multipleOf with large values", + "schema": { + "type": "number", + "multipleOf": 1000000000.5 + }, + "tests": [ + { + "description": "exact large multiple is valid", + "data": 2000000001.0, + "valid": true + }, + { + "description": "large value offset by 0.05 is invalid", + "data": 1000000000.55, + "valid": false + } + ] } ]