Skip to content

Commit 80445d3

Browse files
committed
fix: multipleOf validation for large integers beyond i64::MAX
Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent f279dcd commit 80445d3

File tree

4 files changed

+28
-0
lines changed

4 files changed

+28
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Fixed
6+
7+
- `multipleOf` validation for large u64 values beyond `i64::MAX` with `arbitrary-precision` feature.
8+
59
## [0.37.4] - 2025-11-30
610

711
### Fixed

crates/jsonschema-py/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Fixed
6+
7+
- `multipleOf` validation for large integers beyond `i64::MAX`.
8+
59
## [0.37.4] - 2025-11-30
610

711
### Fixed

crates/jsonschema/src/ext/numeric.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ pub(crate) fn is_multiple_of_float(value: &Number, multiple: f64) -> bool {
8787
}
8888

8989
pub(crate) fn is_multiple_of_integer(value: &Number, multiple: f64) -> bool {
90+
// For large u64 values beyond i64::MAX, as_f64() loses precision (since they exceed 2^53).
91+
// Use u64 arithmetic directly for these cases.
92+
#[cfg(feature = "arbitrary-precision")]
93+
if let Some(v) = value.as_u64() {
94+
// If as_i64() returns None, the value is > i64::MAX, which is > 2^53
95+
// and therefore cannot be exactly represented in f64
96+
if value.as_i64().is_none() {
97+
// Use u64 modulo when the divisor fits in u64
98+
if multiple > 0.0 && multiple <= u64::MAX as f64 && multiple.fract() == 0.0 {
99+
return (v % (multiple as u64)) == 0;
100+
}
101+
}
102+
}
103+
90104
if let Some(value_f64) = value.as_f64() {
91105
// As the divisor has its fractional part as zero, then any value with a non-zero
92106
// fractional part can't be a multiple of this divisor, therefore it is short-circuited

crates/jsonschema/src/keywords/multiple_of.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,12 @@ mod tests {
431431
#[test_case(r#"{"multipleOf": 2}"#, "1e1000", true; "huge scientific integer is even")]
432432
#[test_case(r#"{"multipleOf": 3}"#, "1e1000", false; "10^1000 not multiple of 3")]
433433
#[test_case(r#"{"multipleOf": 0.5}"#, "1e1000", true; "huge scientific integer multiple of 0.5")]
434+
// Regression test: u64 values beyond i64::MAX lose precision when converted to f64.
435+
// 9223372036854775870 ends in '0' and IS a multiple of 10, but when converted to f64
436+
// it becomes 9223372036854775808 which ends in '8' and is NOT a multiple of 10.
437+
#[test_case(r#"{"multipleOf": 10}"#, "9223372036854775870", true; "u64 beyond i64 max multiple of 10")]
438+
#[test_case(r#"{"multipleOf": 10}"#, "9223372036854775871", false; "u64 beyond i64 max not multiple of 10")]
439+
#[test_case(r#"{"type": "integer", "minimum": 9223372036854775800, "maximum": 9223372036854775900, "multipleOf": 10}"#, "9223372036854775870", true; "combined schema with u64 beyond i64 max")]
434440
#[test_case(r#"{"multipleOf": 18446744073709551616}"#, "36893488147419103232", true; "large bigint multiple")]
435441
#[test_case(r#"{"multipleOf": 18446744073709551616}"#, "18446744073709551617", false; "large bigint non-multiple")]
436442
#[test_case(r#"{"multipleOf": 18446744073709551616}"#, "100", false; "small int not multiple of large")]

0 commit comments

Comments
 (0)