You paste an API response into a JSON tool, format it, and copy it back. Everything looks the same — until a database lookup fails because the order ID 1234567890123456789 has quietly become 1234567890123456800. Nothing threw an error, no warning appeared, and the last two digits are simply gone.
This is one of the most common silent data bugs on the web. It is not a bug in JSON itself: it comes from how most programs read JSON. This guide explains where the digits go, which numbers are affected, how to check whether your tools are safe, and how to keep large numbers exact in JavaScript, Python, Go, Java and on the command line.
The Short Answer
JSON numbers have no size limit — the format just says “a sequence of digits”. But JavaScript, and many other runtimes when they parse JSON into a generic structure, store every number as a 64-bit floating-point value (IEEE 754 double). A double has 53 bits for the digits, so it can represent every integer exactly only up to 9,007,199,254,740,991 (253 − 1, Number.MAX_SAFE_INTEGER). Above that, the value is rounded to the nearest number the double can hold.
JSON.parse('9007199254740993') // 9007199254740992
JSON.parse('1234567890123456789') // 1234567890123456800 (printed)
// actually stored: 1234567890123456768
The rounding happens while the text is being parsed, before your code ever sees the value. Any tool that runs your JSON through JSON.parse() and then prints it again — a formatter, a minifier, a logging library, a browser extension — writes the rounded value back out.
Which Numbers Are at Risk?
Ordinary values like prices, counts, ages and coordinates are far below the limit. The danger is in 64-bit integer IDs, which routinely have 18 or 19 digits:
- Snowflake IDs — Twitter/X, Discord and many internal systems generate time-based 64-bit IDs. Twitter’s API has long returned every ID twice, as
id(number) andid_str(string), precisely because JavaScript clients corrupted the numeric version; Discord sends its snowflakes as strings for the same reason. - Database keys —
BIGINTprimary keys, especially from sharded databases or ID generators that start high. - Payment and banking references, order numbers, tracking numbers and other long numeric identifiers.
- Nanosecond timestamps —
1739290000123456789is a perfectly normal value in Go or in tracing systems, and is far above 253. - Hashes and checksums serialised as unsigned 64-bit integers.
A useful rule of thumb: any integer with 16 digits or more deserves a second look; from 17 digits up, a double cannot store all of them.
It Is Not Only Large Integers
Parsing into a double and printing it again also rewrites numbers that are not rounded at all. The value is identical, but the text changes:
| In the original JSON | After JSON.parse + JSON.stringify |
Why it can matter |
|---|---|---|
1.0 |
1 |
A strict schema or another language may now read an integer instead of a float. |
19.90 |
19.9 |
Prices lose their display format; signatures and hashes over the payload no longer match. |
1e2 |
100 |
Usually harmless, but it shows up as a change in diffs. |
-0 |
0 |
The sign of zero is lost (relevant in some scientific data). |
0.1 |
0.1 |
Unchanged on output, but arithmetic on it is still binary floating point: 0.1 + 0.2 is 0.30000000000000004. |
If the JSON is signed, hashed, compared byte-for-byte in a test, or stored in version control, these “cosmetic” changes are real changes. A diff between the original and the re-formatted file will show them — paste both into a diff checker if you are unsure whether a tool touched your numbers.
What the JSON Standard Says
RFC 8259, the JSON specification, does not limit number size, but it does warn about exactly this problem: implementations may set limits, and only integers in the range −(253−1) to 253−1 are guaranteed to be interoperable, meaning every implementation will agree on their value. Anything outside that range is valid JSON, but whether it survives depends on the program reading it. That is why well-designed APIs send big IDs as strings.
How to Check a Tool in Ten Seconds
Paste this into the formatter, viewer or library you want to test and look at the output:
{"id": 1234567890123456789, "edge": 9007199254740993, "price": 19.90, "ratio": 1.0}
If id comes back as 1234567890123456800 or edge as 9007199254740992, the tool parses into doubles and will corrupt real IDs. If 19.90 turns into 19.9, it rewrites number text even where the value is safe.
Our JSON Formatter & Validator uses its own parser that keeps every number exactly as written — 19.90 stays 19.90, long IDs keep all their digits — and it adds a warning when a number is too large to survive JSON.parse() elsewhere, so you know before the value reaches your code.
Keeping Large Numbers Exact in Your Code
The best fix: send IDs as strings
If you control the API, serialise identifiers as strings: {"id": "1234567890123456789"}. An ID is a label, not a quantity — you never add two order numbers together — so a string is the honest type. It works in every language and every tool without special handling. Many APIs do this for all IDs, not just the large ones, so that clients never have to think about it.
JavaScript and TypeScript
Modern JavaScript can finally read the original text of a number. Since ES2026, the reviver function of JSON.parse receives a third argument, context, whose source property holds the raw JSON text of each primitive value:
const data = JSON.parse(text, (key, value, context) => {
if (typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source)) {
return BigInt(context.source); // exact
}
return value;
});
The same proposal added JSON.rawJSON() for the other direction, so you can write a number into JSON without going through a double:
JSON.stringify({ id: JSON.rawJSON('1234567890123456789') })
// '{"id":1234567890123456789}'
Current Chrome, Edge, Firefox and recent Node.js versions support both. If you must support older browsers or runtimes, feature-detect (typeof JSON.rawJSON === 'function'), use the core-js polyfill, or a parsing library such as lossless-json or json-bigint. Note that JSON.stringify throws on a plain BigInt — convert it to a string or use JSON.rawJSON when you write it back.
Python
Python’s integers have arbitrary precision, so json.loads('{"id": 1234567890123456789}') is exact out of the box. Decimal fractions still become float; if you need 19.90 to stay 19.90, parse floats as Decimal:
import json
from decimal import Decimal
data = json.loads(text, parse_float=Decimal)
The catch in Python is usually not the parser but the data’s next stop: as soon as you hand the value to a JavaScript front end, a pandas column of floats or a database column of type DOUBLE, the rounding happens there.
Go
Decoding into a struct field of type int64 or uint64 is exact. Decoding into map[string]any or interface{} is not: every number becomes a float64. Use a decoder with UseNumber() to get a json.Number (the original text) instead:
dec := json.NewDecoder(r)
dec.UseNumber()
var v map[string]any
err := dec.Decode(&v) // numbers arrive as json.Number
When your Go service talks to browsers, the struct tag option `json:"id,string"` writes an int64 as a quoted string and reads it back from one.
Java (Jackson)
Typed fields of type long, BigInteger or BigDecimal are exact. When reading into generic nodes or maps, enable DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS (and, if you need it, USE_BIG_INTEGER_FOR_INTS) so fractions are not turned into double. To protect JavaScript clients, serialise Long IDs as strings with ToStringSerializer or a @JsonFormat(shape = STRING) annotation.
Command line: jq
Older jq versions converted every number to a double, so jq . could change IDs. Since jq 1.7, number literals that you do not modify are printed exactly as they appeared in the input. Arithmetic still converts to a double: .id + 1 on a 19-digit ID is rounded. Check your version with jq --version, especially on older Linux distributions.
Databases
PostgreSQL’s jsonb stores numbers as numeric, which is exact, and MySQL’s JSON type keeps signed and unsigned 64-bit integers exact. Problems start when you extract a value into a DOUBLE/float8 column or let an ORM map it to a float type — keep IDs in BIGINT or text columns.
A Checklist for Teams
- Design: treat identifiers as strings in every public JSON API. Document it in your API schema (
"type": "string", with a pattern of digits if you like). - Front end: never do arithmetic on IDs; compare them as strings. If an API already sends large numeric IDs, parse them with
context.sourceor a lossless library. - Tooling: test your formatters, log viewers and API clients with the ten-second sample above. Many popular ones still parse into doubles.
- Money: do not rely on JSON floats for amounts. Send integer minor units (
1990cents) or decimal strings ("19.90") and convert with a decimal type. - Tests: include one ID above 253 in your fixtures so a regression shows up immediately.
Frequently Asked Questions
What is the largest integer JSON can hold?
JSON itself has no limit. The practical limit is the program reading it: JavaScript and other double-based parsers are exact only up to 9,007,199,254,740,991 (253 − 1). 64-bit integer types go up to 9,223,372,036,854,775,807, and Python integers have no limit at all.
Why does my ID end in 00 after formatting?
The tool parsed the JSON into a 64-bit float, which rounded the ID to the nearest value it can represent, and then printed the shortest decimal form of that float. That form often ends in zeros, but the stored value is different again — 1234567890123456789 becomes 1234567890123456768 internally and is printed as 1234567890123456800.
Is a number above 253 invalid JSON?
No. It is valid JSON, but RFC 8259 notes that such numbers are not interoperable: different parsers may read different values. Sending them as strings avoids the problem entirely.
Can I use BigInt with JSON.stringify?
Not directly — JSON.stringify(10n) throws a TypeError. Convert the value to a string, or wrap the digits with JSON.rawJSON(value.toString()) to write them as an unquoted number.
How can I format JSON without changing any numbers?
Use a formatter that does not round-trip through JSON.parse(). The webutil.io JSON Formatter keeps numbers exactly as written and runs entirely in your browser, so the data never leaves your device. For the basics of the format itself, see our beginner’s guide to JSON.