Why JSON.parse rounds long IDs, and how to fix it
A 64-bit ID goes into JSON.parse and a different number comes out. There is no error and no warning. This guide shows why it happens, which tools do it, and three ways to keep every digit.
The problem in two lines
JSON.parse('{"id":9007199254740993}').id
// 9007199254740992
The last digit changed from 3 to 2. A 19-digit ID loses more: 1234567890123456789 comes back as 1234567890123456800.
It gets worse when you write the data back, because the wrong number is now saved:
JSON.stringify(JSON.parse('{"id":9007199254740993}'))
// {"id":9007199254740992}
Nothing fails. The code keeps running with an ID that belongs to another record, or to no record at all.
Why it happens
JSON itself has no limit on the size of a number. The text 9007199254740993 is valid JSON. The limit comes from the program that reads it.
JSON.parse stores every number as a 64-bit float (IEEE 754 double). A float has 53 bits for the digits, so it can hold every whole number only up to 9,007,199,254,740,991. JavaScript calls that value Number.MAX_SAFE_INTEGER.
Past that point the float can hold only every second whole number, then every fourth, and so on. 9007199254740993 falls between two values a float can hold, so it moves to the nearest one.
The JSON standard warns about this. RFC 8259 says that whole numbers outside the range of ±(253 − 1) may not read back the same in other software.
Where you will meet it
- Database keys. A
bigintprimary key goes past the safe range once the values grow large. - Snowflake IDs. Twitter and Discord use 64-bit IDs. Twitter's API added an
id_strfield besideidfor this reason, and Discord always sends its IDs as strings. - Order and payment numbers. A 19-digit reference from another system can look like a number but must stay exact.
- Precise decimals.
1.2345678901234567890reads back as1.2345678901234567.
Which tools round the number
| Tool | What happens | How to keep the digits |
|---|---|---|
JavaScript JSON.parse | Rounds | Use a reviver or a lossless parser (below) |
Go json.Unmarshal into any | Rounds, because it uses float64 | Call Decoder.UseNumber(), or use an int64 field |
Python json.loads | Whole numbers stay exact, decimals round | Pass parse_float=Decimal |
| jq 1.7 and later | Keeps the digits until you do maths on them | Do not calculate with the ID |
| Excel | Keeps 15 digits, the rest become zeros | Store the ID as text |
We ran the code rows on 2 November 2026 with Node 24, Go 1.27, Python 3.13 and jq 1.8.2.
Three ways to keep every digit in JavaScript
1. Send the ID as a string
If you own the API, this is the best fix. A string cannot be rounded, and every JSON reader handles it the same way.
{"id": "9007199254740993"}
An ID is a label. You never add two order numbers together, so it loses nothing by being a string.
2. Read the source text with a reviver
If you do not own the API, you can read the original text of each number. The reviver of JSON.parse receives it as context.source:
function keepLongIds(key, value, context) {
if (typeof value !== 'number' || Number.isSafeInteger(value)) return value;
return /^-?\d+$/.test(context.source) ? BigInt(context.source) : value;
}
const doc = JSON.parse(text, keepLongIds);
doc.id // 9007199254740993n
JSON.stringify refuses a BigInt, so use JSON.rawJSON to write the digits back:
JSON.stringify(doc, (key, value) =>
typeof value === 'bigint' ? JSON.rawJSON(String(value)) : value);
// {"id":9007199254740993}
This works in Chrome 114, Firefox 135 and Safari 18.4 or later, and in Node 24.
3. Use a lossless parser
A lossless parser keeps each number as its original text until you ask for it. jsonloupe uses lossless-json for this:
import { parse, stringify } from 'lossless-json';
const doc = parse('{"id":9007199254740993}');
String(doc.id) // '9007199254740993'
stringify(doc) // '{"id":9007199254740993}'
How to check if your viewer rounds numbers
A JSON viewer built on JSON.parse shows you the rounded number. If you copy that ID into a query or a support ticket, you look up the wrong record. Paste this into your viewer or debug tool:
{"id": 9007199254740993, "price": 1.2345678901234567890}
If you see 9007199254740992, the tool changed the value before you read it.
Common questions
Is 9007199254740993 valid JSON?
Yes. The JSON grammar has no limit on the size of a number. Only the program that reads it has one.
Does TypeScript catch this?
No. Types are removed before the code runs. Writing id: bigint on an interface does not change what JSON.parse returns.
Does BigInt fix it?
Only with the steps above. BigInt holds whole numbers of any size, but JSON.parse never creates one, and JSON.stringify throws “Do not know how to serialize a BigInt”.
Check your own file. jsonloupe shows every number exactly as the file has it, and nothing is uploaded. Open jsonloupe →