BigInt
The problem in number
In some special scenarios, the backend may return such a string:
'{"value": 9223372036854775807}'; // 19-digits
If you use JSON.parse ()
to parse directly:
JSON.parse('{"value": 9223372036854775807}');
// {value: 9223372036854776000}
We will find that the value responded by the backend is too large. In JavaScript, all numbers are saved as 64-bit
float, which results in two restrictions on the representation of the values:
53: Precision
The largest integer, JavaScript is able to exactly represent with number
, is 2**53
. There is also a constant Number.MAX_SAFE_INTEGER
available you can access from your code that returns 2**53 - 1
, the largest integer where you can add 1
and still get an exact result:
Number.MAX_SAFE_INTEGER; // 9007199254740991, 2**53 -1
Number.MAX_SAFE_INTEGER + 1; // 9007199254740992
Number.MAX_SAFE_INTEGER + 2; // 9007199254740992 (wrong!!!)
The precision of the value can only reach 53 binary digits (equivalent to 16 decimal digits).
Integers larger than this range cannot be accurately represented by JavaScript, which makes JavaScript unsuitable for accurate scientific and financial calculations.
2 ** 53 === 2 ** 53 + 1; // true
1024: Infinity
Any value greater than or equal to 2**1024
that cannot be represented by JavaScript and returns Infinity
.
2 ** 1024; // Infinity
What's BigInt
Before solve above problem, let's know more about BigInt
.
BigInt
(ES2020) is a built-in object that provides a way to represent whole numbers larger than 2**53 - 1
, which is the largest number JavaScript can reliably represent with the Number primitive and represented by the Number.MAX_SAFE_INTEGER
constant. BigInt
can be used for arbitrarily large integers.
There is no restriction on the number of digits, and any digit integer can be accurately represented.
const a = 2172141653n;
const b = 15346349309n;
// Result maintains precision even with 20-digits
a * b; // 33334444555566667777n
// Normal number cannot maintain precision
Number(a) * Number(b); // 33334444555566670000
Cannot add n
after decimal:
0.1n; // Uncaught SyntaxError: Invalid or unexpected token
BigInt
can also be expressed in various bases, all with the suffix n
.
0b1101n; // Binary
0o777n; // Octonary
0xffn; // Hex
BigInt
and normal number are not equal:
42n === 42; // false
When tested against typeof
, a BigInt
will give "bigint"
:
typeof 123n; // 'bigint'
Also unsupported is the unary operator (+), in order to not break asm.js.
-42n; // OK
+42n; // Uncaught TypeError: Cannot convert a BigInt value to a number
You can use the toString ()
method to convert to a string
, and the suffix n
will be removed automatically:
let a = 123n;
a.toString(); // "123"
Solution1: Sending number as string in raw string
This solution is easier to be implemented in frontend while a little complicated in backend. It has to convert the String
to long
when server get the value. Let's see the overview workflow first:
Figure 1: sending number as string
Unfortunately, JSON does not support BigInt
. If you try to serialize an object with BigInt
values in it, the methods JSON.stringify()
throw errors:
// Serialize
JSON.stringify({value: 9223372036854775807n});
// Uncaught TypeError: Do not know how to serialize a BigInt at Object.stringify
Another issue is that JSON.parse
will return a value of string when deserialize, actually what we want is BigInt
:
// Deserialize
JSON.parse('{"value": "9223372036854775807"}');
// {value: "9223372036854775807"} but we want {value: 9223372036854775807n}
Next we will add reviver
or replacer
function in JSON
to resolve above issues.
Serialization: JSON.stringify()
As we have seen in the above section, serializing an object with JSON.stringify()
throws an error if it contains properties with BigInt
values. Fortunately, this method also supports a second optional parameter called replacer
. A function that is called for each key/value
pair. In this replacer
function, we check the data type, and when it's a BigInt
convert it to a string
:
function stringifyReplacer(key, value) {
if (typeof value === 'bigint') {
return value.toString();
}
return value;
}
And now when we call JSON.stringify()
with stringifyReplacer
, it will returned the value as we expected:
JSON.stringify({value: 9223372036854775807n}, stringifyReplacer);
// '{"value": "9223372036854775807"}'
You can also implement your own toJSON
method on BigInt.prototype
if needed:
BigInt.prototype.toJSON = function () {
return this.toString();
};
Instead of throwing, JSON.stringify
now produces a string like this:
JSON.stringify({value: 9223372036854775807n});
// '{"value": "9223372036854775807"}'
Deserialization: JSON.parse()
As mentioned above, if we simply call JSON.parse()
on JSON
, we get back a string
, but we want an object with a BigInt
property.
The JSON.parse()
method supports an optional second parameter called reviver
. This is a function that is called for each key/value
pair. Here we check if the value is a string
, contains just numbers, and the letter n
at the end. If that is the case, convert it to a BigInt
:
function parseReviver(key, value) {
if (typeof value === 'string' && /^\d+/.test(value)) {
return BigInt(value);
}
return value;
}
Let's see what will be returned if we call JSON.parse()
with parseReviver
:
JSON.parse('{"value": "9223372036854775807"}', parseReviver);
// {value: 9223372036854775807n}
Solution2: Sending number directly in raw string
This solution is a bit hard to be implemented in frontend but the benefit is that no code needs to changed in backend. Below is the overview workflow of this solution:
Figure 2: sending number directly
Use text()
in fetch
Unfortunately, the json()
method of the fetch
response object does not support the reviver
argument, so we can't use this pattern:
const response = await fetch('fetchData');
const obj = response.json();
Instead, we have to access the raw string response with text()
:
const response = await fetch('fetchData');
const obj = response.text();
Use json-bigint
We recommend json-bigint to handle serialization and deserialization as the following shows:
import JOSNbig from 'json-bigint';
JSONbig.parse('{"value": 9223372036854775807}');
// {value: 9223372036854775807n}
JSONbig.stringify({value: 9223372036854775807n});
// '{"value": 9223372036854775807}'
In addition, below is a simple implementation for parse
, you can use as a reference:
let handleString = stringData =>
stringData.replace(/:\s*([-+Ee0-9.]+)/g, ': "uniqueprefix$1"');
let parse = stringData =>
JSON.parse(handleString(stringData), (key, value) => {
// only changing strings
if (typeof value !== 'string') return value;
// only changing number strings
if (!value.startsWith('uniqueprefix')) return value;
// chip off the prefix
value = value.slice('uniqueprefix'.length);
// pick your favorite arbitrary-precision library
return BigInt(value);
});
parse('{"value": 9223372036854775807}');
// {value: 9223372036854775807n}
Browser compatibility
Currently, all modern browsers except Safari support it.