<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jack Smith</title>
    <description>The latest articles on DEV Community by Jack Smith (@jacksmith_de).</description>
    <link>https://dev.to/jacksmith_de</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3862007%2F926aa077-69ff-4418-b4a4-850c87582762.png</url>
      <title>DEV Community: Jack Smith</title>
      <link>https://dev.to/jacksmith_de</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jacksmith_de"/>
    <language>en</language>
    <item>
      <title>We Built Guardrails Against JSON Precision Loss. Then We Found Out Half of Them Weren't Actually Guarding Anything.</title>
      <dc:creator>Jack Smith</dc:creator>
      <pubDate>Thu, 20 Aug 2026 23:19:40 +0000</pubDate>
      <link>https://dev.to/jacksmith_de/we-built-guardrails-against-json-precision-loss-then-we-found-out-half-of-them-werent-actually-4nk2</link>
      <guid>https://dev.to/jacksmith_de/we-built-guardrails-against-json-precision-loss-then-we-found-out-half-of-them-werent-actually-4nk2</guid>
      <description>&lt;p&gt;A few weeks ago I ran this through our own JSON repair tool, in the browser, on our own website:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22233344455566677788&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It came back as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22233344455566676000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No error. No warning. Just a quietly wrong number, presented as a successful repair.&lt;/p&gt;

&lt;p&gt;That number is a made-up snowflake ID, the kind of thing you'd see as a primary key in a distributed system. And our own tool — the one whose entire pitch is "we don't ship plausible-looking but wrong results" — had just silently corrupted it and told the user everything was fine.&lt;/p&gt;

&lt;p&gt;Why this happens, and why it's not really a bug in the traditional sense&lt;/p&gt;

&lt;p&gt;JSON doesn't distinguish between integers and floats. It just has "number." JavaScript doesn't either — every number, however you write it, ends up as an IEEE 754 double-precision float once &lt;strong&gt;JSON.parse&lt;/strong&gt; gets to it. Doubles can represent integers exactly up to &lt;strong&gt;Number.MAX_SAFE_INTEGER&lt;/strong&gt; (2^53 - 1, which is 16 digits). Past that, the value gets silently rounded to the nearest representable double. No exception is thrown. No flag is set. The number that comes out the other side is just wrong, and there's nothing in the return value that tells you so.&lt;/p&gt;

&lt;p&gt;This isn't a JavaScript-specific bug you can code around after the fact. Once a value has passed through &lt;strong&gt;JSON.parse&lt;/strong&gt; and become a JS Number, the original precision is gone. There's no repair step that can reconstruct it, because the information needed to reconstruct it no longer exists. The only two options are: catch it before parsing and refuse to proceed, or let it happen and hope nobody notices.&lt;/p&gt;

&lt;p&gt;Other systems handle this differently — Twitter has shipped both a numeric id and a string id_str in the same response for exactly this reason, so JS clients have a safe fallback. Stripe represents money as integer cents rather than a decimal, specifically to avoid a related class of float bug. Even pandas — one of the most widely used data libraries in existence — has open GitHub issues going back to 2017 about to_json() silently mangling large integers, still unresolved. This isn't an obscure edge case. It's a well-known, unsolved-by-default problem that most tools quietly hope you never hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  We already had a fix for this. We just hadn't applied it everywhere.
&lt;/h2&gt;

&lt;p&gt;Here's the part that actually embarrassed us. Our API had a guard for exactly this — before any of our four processing engines (clean, schema-validate, transform, AI-repair) touches your input, it scans the raw text for numbers that would lose precision if parsed, and rejects the request outright with a 422 and a clear explanation, before any parsing happens.&lt;/p&gt;

&lt;p&gt;That protection lived in a shared core module used by all four engines on the server. What we hadn't fully internalized was that three of those same tools also ship a browser-only version — a client-side page where the JSON never leaves your machine. And the browser pages weren't calling the same guard. They were calling JSON.parse directly, the same way any quick script would.&lt;/p&gt;

&lt;p&gt;So the server was safe. The browser tools, running the exact same "repair" logic under a different roof, were not. Two code paths, one contract, and only one of them actually honored it.&lt;/p&gt;

&lt;p&gt;We found this the boring way — not through a bug report, but by going page by page and deliberately feeding oversized numbers into every tool we ship, checking whether each one caught it or quietly let it through. Three out of four browser pages let it through. One of them — the CSV-to-JSON converter — had an even sneakier version of the same problem: the CSV text is submitted as a single JSON string field, so a naive scan of the outer request sees no bare numbers at all. The numbers are hiding inside a string until the moment they get parsed out of the CSV rows, which is exactly where they got quietly rounded.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part we got wrong on purpose, and then had to unwind
&lt;/h2&gt;

&lt;p&gt;Before we fixed any of this, we'd actually designed the large-integer check to be non-blocking on purpose — log it, don't reject it. The reasoning seemed sound: some systems legitimately transmit 64-bit database keys or snowflake IDs as raw JSON numbers, and we didn't want to break real traffic without knowing whether anyone depended on that.&lt;/p&gt;

&lt;p&gt;Once we sat down and actually reasoned through it, that compromise didn't hold up. There is no code path that "fixes" an oversized integer without first losing precision — the same math that makes rejection necessary for a five-digit decimal makes it necessary for a twenty-digit integer. A warn-only mode doesn't prevent corruption; it just adds a log line next to the corruption. We checked our own traffic and found we had no real callers depending on the old behavior yet, which meant there was no cost to closing the gap immediately rather than "eventually." So we did — decimal precision and integer precision are now checked identically, everywhere, always rejecting before parsing rather than after.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "before parsing" actually looks like now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://json.flyingfishspace.com/api/json/v1/clean &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"id": 22233344455566677788}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Input contains integer literal(s) exceeding safe precision (max 16 digits, Number.MAX_SAFE_INTEGER = 9007199254740991): &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;22233344455566677788&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; (20 digits). Encode large integers as strings instead to preserve exact values."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikeh283axajr256f7gu6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikeh283axajr256f7gu6.png" alt=" " width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No charge for a rejected request. No number silently changed. If your data has values that would lose precision, you find out immediately, with the exact literal that triggered it — not three steps later when a downstream system does math on a number that's already wrong.&lt;/p&gt;

&lt;p&gt;The browser tools now run the identical check, client-side, before JSON.parse ever runs — including the CSV importer, which now scans the embedded CSV text as its own pass, separate from the outer request.&lt;/p&gt;

&lt;p&gt;If you're curious what the tools actually look like: &lt;em&gt;&lt;strong&gt;&lt;u&gt;json.flyingfishspace.com&lt;/u&gt;&lt;/strong&gt;&lt;/em&gt; — the source of this whole story.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>javascript</category>
      <category>programming</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
