A client of mine has an API that returns a large 64bit integer as public IDs for database records that is consumed by a simple Javascript/jquery frontend. The application is rather small and requires no major frontend engineering which is why this simple setup was chosen.
To their surprise, when they passed a large integer from the backend API to the frontend, Javascript would silently parse the result incorrectly.
# API
GET /some/endpoint
{ "id": 1740970989897365796, ... }
# Front end
JSON.parse('{"id": 1740970989897365796, ...}')
Result: Object { id: 1740970989897365800, ... } // WTF?
This happens because Javascript is a language beyond redemption. All numbers in JS are represented as floating point numbers that cap at 253. And because of that, when a large integer is passed, rather than terminating the script or throwing a conversion error, the language silently “truncates” anything outside of the acceptable numeric range
Acceptable range: -9007199254740992 (-2^53) to 9007199254740992 (2^53)
While this is technically within spec, it is beyond comprehension that this is the actual spec. Ideally, what the JSON.parse function should do is either fail with an error or convert the big integer to string, for which there is no option as far as I know.
To make matters worse, any attempt at converting the incoming raw json string will always result in the same error as any attempt at parsing it without a custom parser, like Bigint, will inevitably end up in the same mess. Regex is also not ideal because there are too many edge cases to consider.
So how does one solve for this?
Well, if you don’t control the backend API, your only reasonable choice is to custom parse the incoming raw JSON string with the help of Bigint library or its equivalent. That’s especially true if you want backwards compatibility across the board. If you do control the backend API however, make sure to output the result as a string rather than an integer.
Ironically, PHP, which is the backend language of choice for the project, does not come with a builtin JSON_BIGINT_AS STRING flag in its JSON_ENCODE function. so… a little bit of work is needed ahead of time.
# Original API output:
{ "id": 1740970989897365796, ... }
# Code used to generate the output
echo json_encode($data);
# ======================
# New modified output
{ "id": "1740970989897365796", ... }
# Code used to generate the new output using recursion:
echo json_encode(BigintToString($data));
function BigintToString(mixed $data): mixed {
// 32 bit int limits
$max32Bit = 2147483647;
$min32Bit = -2147483648;
// Recursively convert large integers to strings
if (is_array($data) || is_object($data)) {
foreach ($data as $key => $value) {
$data[$key] = BigintToString($value);
}
}
else if (is_int($data) && ($data > $max32Bit || $data < $min32Bit)) {
return (string) $data;
}
return $data;
}
What the function above will do is recursively convert any number above the 32bit limit to a string. Technically, we don’t need to convert everything above 32bit – we only need to convert numbers above the 53bit limit. But, by using 32 instead, we can just make our lives a whole lot easier. Any number below 32bit will stay as an integer, anything above it will be outputted as a string. Simple.