Skip to content

Build1 publisher3 min readPublished

A 20-digit ID went into a JSON repair tool and a different number came out

A team that sells precision guardrails found its own browser pages calling JSON.parse directly, rounding a snowflake ID by 1,788 with no error. The server path was safe. The client path was not.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying A 20-digit ID went into a JSON repair tool and a different number came out
Generated illustration

What happened

  • The team ran {"id": 22233344455566677788} through its own JSON repair tool in the browser and it returned {"id": 22233344455566676000}, with no error and no warning, presented as a successful repair.
  • The number used in the test is a made-up snowflake ID, the kind of value seen as a primary key in a distributed system.
  • JSON does not distinguish integers from floats, it has only "number"; in JavaScript every number becomes an IEEE 754 double-precision float once JSON.parse processes it.
  • Doubles represent integers exactly up to Number.MAX_SAFE_INTEGER (2^53 - 1, which is 16 digits); past that the value is silently rounded to the nearest representable double, with no exception thrown and no flag set.
  • Once a value has passed through JSON.parse and become a JS Number the original precision is gone and no repair step can reconstruct it; the only options are to catch it before parsing and refuse to proceed, or let it happen.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

A team that builds JSON repair tooling ran `{"id": 22233344455566677788}` through its own browser-based tool and got back `{"id": 22233344455566676000}` with no error and no warning, presented as a successful repair [1]. The number was a made-up snowflake ID, the shape of thing that serves as a primary key in a distributed system [2], which means the failure mode is not cosmetic: it is a silently rewritten row identifier.

The mechanism is boring and well documented. JSON has one numeric type, and JavaScript resolves every number to an IEEE 754 double once `JSON.parse` touches it [3]. Doubles hold integers exactly up to `Number.MAX_SAFE_INTEGER`, 2^53 - 1, which is 16 digits; beyond that the value rounds to the nearest representable double with no exception thrown and no flag set [4]. The test ID is 20 digits, four digits past the exact range [2] and roughly 2,470 times `MAX_SAFE_INTEGER` [3]. The output was 1,788 short of the input, with the last four digits flattened to zeros [1]. There is no recovery step afterwards, because the information needed to reconstruct the original is gone; the choices are to catch it before parsing and refuse, or let it through [5].

The organisational half is the more useful part. According to the dev.to writeup, the team's API already had the right guard: a shared core module scanned raw request text for numbers that would lose precision and rejected with a 422 before any of four processing engines (clean, schema-validate, transform, AI-repair) parsed anything [9]. Three of those tools also ship browser-only pages, and those pages called `JSON.parse` directly instead of the shared guard [10]. Nobody filed a bug. The team found it by walking page by page and feeding oversized numbers into everything it ships, and three of four browser pages let the value through [11]. The counts in the post do not quite reconcile, three tools with browser versions against three of four failing pages, which is worth flagging if you read this as an audit rather than a confession [11].

One case is worth copying into your own test plan. The CSV-to-JSON converter submits the CSV as a single JSON string field, so a scan of the outer request sees no bare numbers at all; the digits stay inside a string until the CSV rows are parsed, which is exactly where the rounding happened [12]. A guard at the front door does nothing if text becomes numbers somewhere deeper in.

The team also reports it had originally made the large-integer check non-blocking on purpose, logging rather than rejecting, on the reasoning that some systems legitimately transmit 64-bit keys and snowflake IDs as raw JSON numbers [13]. It later concluded that warn-only does not prevent corruption, it only puts a log line next to the corruption, since no code path repairs an oversized integer without first losing it [14]. The supplied text cuts off mid-sentence as the author describes checking their own traffic, so the traffic finding itself is not available [15].

None of this is novel. Twitter has shipped a numeric `id` and a string `id_str` in the same response for this reason [6], Stripe represents money as integer cents [7], and pandas has open GitHub issues dating to 2017 about `to_json()` mangling large integers, still unresolved [8].

What to watch: whether your own ingest rejects oversized integers or merely logs them [14], whether your client-side and edge paths share the server's validation code or reimplement it [10], and whether any payload arrives with numbers wrapped inside string fields that your outer scan cannot see [12]. When you buy an API, ask for string-typed identifiers [6].

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories