Skip to content

Commit b15c580

Browse files
authored
Support BigInt in the JS port (#722)
Fixes: #721 Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 1533109 commit b15c580

11 files changed

Lines changed: 619 additions & 380 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,10 @@ jobs:
148148
- run: cmake --build ./build --config Release --verbose --target benchmark_json
149149
if: matrix.platform.benchmark
150150

151-
# Node.js
151+
- uses: actions/setup-node@v4
152+
with:
153+
node-version: 'latest'
154+
- run: node --version
152155
- run: node --test ports/javascript/test.mjs ports/javascript/trace.mjs
153156
- run: node ports/javascript/benchmark.mjs > build/benchmark-javascript.json
154157
if: matrix.platform.benchmark

ports/javascript/README.md

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,34 @@ const result = evaluator.validate(instance,
6868
console.log(result); // true or false
6969
```
7070

71+
### Parsing large integers
72+
73+
JavaScript's
74+
[`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse)
75+
silently truncates integers that exceed
76+
[`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
77+
to IEEE 754 double-precision floats. If your schemas or instances contain large
78+
integers, pass `Blaze.reviver` to `JSON.parse` to preserve them as
79+
[`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)
80+
values. This relies on the
81+
[`JSON.parse` source text
82+
access](https://github.com/tc39/proposal-json-parse-with-source) proposal
83+
(Stage 4, shipped in all major engines):
84+
85+
```javascript
86+
const template =
87+
JSON.parse(readFileSync("template.json", "utf-8"), Blaze.reviver);
88+
const evaluator = new Blaze(template);
89+
90+
const instance =
91+
JSON.parse(readFileSync("instance.json", "utf-8"), Blaze.reviver);
92+
console.log(evaluator.validate(instance));
93+
```
94+
95+
The evaluator handles `BigInt` values natively in all numeric instructions.
96+
Without a reviver, large integers are silently truncated and validation may
97+
produce incorrect results for affected values.
98+
7199
## Why compile separately?
72100

73101
Unlike validators that compile and evaluate in a single step, Blaze separates
@@ -95,24 +123,18 @@ against your data, with no knowledge of JSON Schema itself.
95123

96124
## Limitations
97125

98-
**No high-precision decimal support.** Compiled templates preserve
99-
arbitrary-precision numbers exactly as they appear in your schemas. However,
100-
JavaScript's
101-
[`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse)
102-
silently truncates all numbers to IEEE 754 double-precision floats before the
103-
evaluator ever sees them. While JavaScript has
104-
[`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt),
105-
[`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse)
106-
does not use it. This is a language-level limitation that affects any numeric
107-
keyword that depends on exact arithmetic. For example:
126+
**No arbitrary-precision real number support.** Large integers can be preserved
127+
as `BigInt` using a reviver (see above), but JavaScript has no equivalent type
128+
for arbitrary-precision real numbers. `JSON.parse` truncates values like
129+
`0.1000000000000000000000000000000001` to IEEE 754 doubles, and there is no
130+
reviver-based workaround:
108131

109-
```sh
110-
$ node --eval "console.log(JSON.parse('9007199254740993'))"
111-
9007199254740992 # Note the result is off by 1
132+
```
133+
$ node --eval "console.log(JSON.parse('0.1000000000000000000000000000000001'))"
134+
0.1
112135
```
113136

114-
The TC39 [`JSON.parse` source text
115-
access](https://github.com/tc39/proposal-json-parse-with-source) proposal
116-
(Stage 4, shipped in all major engines) provides a path forward by exposing the
117-
raw source text to a reviver function. We plan to take advantage of this in a
118-
future release.
137+
Numeric keywords like `multipleOf` that depend on exact decimal arithmetic may
138+
produce incorrect results for real values that lose precision during parsing.
139+
The TC39 [Decimal proposal](https://github.com/tc39/proposal-decimal) (Stage 2)
140+
aims to address this in the future.

ports/javascript/compile.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
22
import { existsSync } from 'node:fs';
33
import { resolve, dirname } from 'node:path';
44
import { fileURLToPath } from 'node:url';
5+
import { Blaze } from './index.mjs';
56

67
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
78
const PROJECT_ROOT = resolve(MODULE_DIR, '../..');
@@ -49,5 +50,5 @@ export function compileSchema(schemaPath, options) {
4950
throw new Error(result.stderr.trim());
5051
}
5152

52-
return JSON.parse(result.stdout);
53+
return JSON.parse(result.stdout, Blaze.reviver);
5354
}

ports/javascript/index.d.mts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export type EvaluationCallback = (
1010
) => void;
1111

1212
export declare class Blaze {
13+
static reviver(
14+
key: string,
15+
value: unknown,
16+
context: { source: string }
17+
): unknown;
1318
constructor(template: Template);
1419
validate(instance: unknown, callback?: EvaluationCallback): boolean;
1520
}

0 commit comments

Comments
 (0)