Developer tools · JSON formatter & validator
Large integer IDs in JSON: why JavaScript formatters may round them
· Why it matters
json developer-workflow validation
JSON allows integers of any size, but JavaScript represents numbers as 64-bit floats, so anything above 2^53 can change when parsed and re-serialised. This post explains the limit, how to spot the damage, and how to protect IDs.
The ID that changed by one
The ID that changed by one often arrives as perfectly valid JSON. Put `{"orderId":9007199254740993}` into JavaScript and `JSON.parse` returns a Number whose displayed value is `9007199254740992`. Parsing succeeds because the token follows JSON number grammar; the damage occurs while converting those decimal digits into JavaScript’s numeric representation. A formatter that serializes the parsed value faithfully writes the rounded Number, not the exact token that appeared in the source.
The contrast is immediate when the same digits are quoted. `JSON.parse("{"orderId":"9007199254740993"}")` returns the string `9007199254740993`, preserving every character, and `JSON.stringify` emits those digits unchanged inside quotes. This is why syntax validation alone cannot protect a numeric identifier. Compare input and output whenever long integers appear, and treat identifiers as strings at the producing boundary when arithmetic is not part of their meaning.
What RFC 8259 says about numbers
RFC 8259 defines the spelling of a JSON number but does not give every implementation an arbitrary-precision numeric type. The grammar permits an optional minus sign, an integer portion, and optional fraction and exponent portions. It excludes conveniences such as hexadecimal notation, `NaN` and `Infinity`. Consequently, `9007199254740993` is syntactically valid even though a common JavaScript consumer cannot represent that integer exactly as a Number.
The specification’s interoperability guidance is the practical warning: software commonly uses IEEE 754 binary64 numbers, and integers in the range from negative `2^53 + 1` through positive `2^53 - 1` are interoperable in the sense of exact agreement. A validator can correctly accept a larger token while a parser later rounds it.
Where 2^53 comes from
The `2^53` boundary comes from the precision available in a binary64 significand. JavaScript exposes the highest consecutively representable integer as `Number.MAX_SAFE_INTEGER`, which is `9007199254740991`. At and below that magnitude, adjacent integers can be represented distinctly. Above it, the spacing between representable values grows, so some neighboring decimal integers map to the same Number. The runtime is not truncating a string; it is selecting the nearest value available in that finite binary format.
A revealing console check is `Number.isSafeInteger(9007199254740993)`, which is false, although the source literal has already been rounded before the function receives it. Another is `9007199254740992 === 9007199254740993`, which evaluates true in JavaScript. These examples concern exact integer identity, not whether every larger number becomes unusable.
How parse-and-reserialise loses digits
Parse-and-reserialize formatting has three stages: read numeric characters, create an in-memory value, then generate fresh characters from that value. Lexical details disappear at the middle stage. With `{"ticket":9223372036854775807}`, `JSON.parse` creates the nearest available JavaScript Number; `JSON.stringify` then emits `9223372036854776000`. The serializer is not independently corrupting a preserved token. By serialization time, the original sequence of digits is no longer present in the parsed object.
ToolAcre’s repository implementation uses `JSON.parse` and `JSON.stringify`, so this limitation applies to its formatted output. Its syntax scanner runs to provide a stable reason and location after parsing fails; it does not replace JavaScript numbers with an arbitrary-precision representation. A successful validation result therefore establishes grammar, while a formatting diff can reveal precision loss.
Worked example: comparing input and output
Compare `{"numeric":9007199254740993,"text":"9007199254740993"}` before and after a JavaScript round trip. Running `JSON.stringify(JSON.parse(source), null, 2)` produces a formatted object whose `numeric` member is `9007199254740992`, while `text` remains `"9007199254740993"`. Both members were valid in the input, and both remain valid in the output. Only the quoted representation preserves the identifier exactly because it is decoded as character data rather than a Number.
A useful review does not merely ask whether the formatter showed green. Search the source for uninterrupted digit sequences, compare any values longer than the safe range, and determine whether each field represents a quantity or an opaque label. If the producer controls the contract, change the label to a string there and document that choice for consumers.
Protecting IDs at the source
Protect IDs at the source by defining them as strings in the schema and serializing them as strings before any JavaScript client receives the payload. An ID may contain only digits and still be nonnumeric in meaning: addition, rounding and ordering by magnitude are not legitimate operations on an account key. A string also preserves leading zeroes, which a numeric representation would discard even when its magnitude is within the safe range.
Do not infer cross-language safety from the fact that another runtime can hold a larger integer. Parsers and target types vary, and an intermediary written in JavaScript can round the value before a later service sees it. Some specialized parsers preserve number tokens or construct big integers, but every participant must share that contract.
What this does not cover
What this does not cover is the broader design of decimal arithmetic. Values such as `0.1` have their own binary floating-point behavior, and money may require scaled integers or decimal types according to the application contract. Nor does quoting every number automatically improve a schema. Counts, coordinates and measurements are often legitimately numeric. The decision depends on whether exact decimal spelling or exact integer identity must survive every consumer in the data path.
This discussion also does not claim that JSON itself rounded the token or that all parsers behave like JavaScript. The concrete repository evidence is narrower: this formatter calls `JSON.parse` and `JSON.stringify`, so JavaScript Number semantics govern unquoted values here. An arbitrary-precision JSON library can make different choices, but it must define how values are exposed and serialized.
Takeaway: numbers above 2^53 belong in strings
The takeaway is specific: integer identifiers outside JavaScript’s safe range belong in strings when they must pass through JavaScript without changing. `9007199254740993` as a JSON number is valid syntax but becomes `9007199254740992` after `JSON.parse`; `"9007199254740993"` remains exact. The quotes are not decoration. They select a representation that preserves the digits as data and prevents consumers from treating an opaque label as an approximate quantity.
Before replacing a document with formatter output, compare long numbers against the original and investigate every changed digit. Fix the producer and schema when possible so all downstream clients receive the safe form consistently. ToolAcre can expose the consequence because its output reflects the parsed JavaScript value, but it cannot reconstruct digits already lost during parsing.