<?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: xushoulin</title>
    <description>The latest articles on DEV Community by xushoulin (@xushoulin).</description>
    <link>https://dev.to/xushoulin</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%2F4201742%2Fcc752346-916c-4221-b1d1-0239a382c4a9.jpg</url>
      <title>DEV Community: xushoulin</title>
      <link>https://dev.to/xushoulin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xushoulin"/>
    <language>en</language>
    <item>
      <title>Unexpected token '&lt;' in JSON at position 0? Your API returned HTML</title>
      <dc:creator>xushoulin</dc:creator>
      <pubDate>Fri, 30 Oct 2026 02:39:55 +0000</pubDate>
      <link>https://dev.to/xushoulin/unexpected-token-in-json-at-position-0-your-api-returned-html-352o</link>
      <guid>https://dev.to/xushoulin/unexpected-token-in-json-at-position-0-your-api-returned-html-352o</guid>
      <description>&lt;p&gt;A support ticket I have seen more than any other, in different disguises:&lt;br&gt;
"The API works in Postman. In our app it throws Unexpected token '&amp;lt;' in JSON at position 0."&lt;br&gt;
Every time, the same thing has happened: the app never received JSON. It received an HTML page, and the parser choked on the first character before it could learn anything about the payload.&lt;br&gt;
That distinction sounds trivial. It costs people an afternoon, because the error message points at a character in a document that was never the problem.&lt;br&gt;
Position 0 means "I never read a value"&lt;br&gt;
A JSON document can only begin with one of seven things:&lt;br&gt;
{   [   "    -   0-9   t (true)   f (false)   n (null)&lt;br&gt;
A strict parser reads the first non-whitespace character, checks it against that list, and stops immediately if it does not match.&lt;br&gt;
An angle bracket matches nothing on that list. So the parser gives up before reading a value, reports position 0, and has no information about your data — because it never touched your data. The response is not malformed JSON. It is not JSON.&lt;br&gt;
Which means: editing the payload will do nothing. The bug is upstream of the payload.&lt;br&gt;
What actually arrived&lt;br&gt;
Seven causes cover almost every case. The middle column is the part worth memorising, because it decides whether your code can even tell that something went wrong.&lt;br&gt;
Cause&lt;br&gt;
Body you received&lt;br&gt;
How to recognise it&lt;br&gt;
Route does not exist&lt;br&gt;
HTML 404 page&lt;br&gt;
Status 404, body starts with &amp;lt;!DOCTYPE or contains Cannot GET&lt;br&gt;
Handler threw&lt;br&gt;
HTML 500 page&lt;br&gt;
Status 500, in development usually a stack trace&lt;br&gt;
SPA catch-all rewrite&lt;br&gt;
index.html, status 200&lt;br&gt;
Any unknown path returns the app shell&lt;br&gt;
Auth redirect&lt;br&gt;
The login page&lt;br&gt;
fetch followed the 302, so you get its 200&lt;br&gt;
Reverse proxy / CDN&lt;br&gt;
nginx, Cloudflare or LB page&lt;br&gt;
502–504, server: nginx header, app never ran&lt;br&gt;
Trailing slash mismatch&lt;br&gt;
A 301, then the page&lt;br&gt;
Check res.url, not the URL you requested&lt;br&gt;
Wrong base URL&lt;br&gt;
Whatever lives at that host&lt;br&gt;
A typo, or http where you meant https&lt;br&gt;
Only three of those come with a non-200 status. The other four are the interesting ones.&lt;br&gt;
The 200 that lies&lt;br&gt;
Deploy a static host with the standard single-page-app rewrite and every unknown path answers with the app shell and a 200.&lt;br&gt;
So a request for a route that does not exist returns a response that looks perfectly healthy. fetch resolves. res.ok is true. res.statusText is OK.&lt;br&gt;
The only thing that fails is res.json(), and its message blames a character.&lt;br&gt;
This is the case where checking the status code cannot save you, because the status code is correct. What still tells the truth is the content-type header — which is why the fix below checks it before it parses anything.&lt;br&gt;
Three ways to see the body in ten seconds&lt;br&gt;
From a terminal. -i prints the status line and headers above the body, which is normally all you need:&lt;br&gt;
curl -i -H 'Accept: application/json' &lt;a href="https://api.example.com/v1/orders" rel="noopener noreferrer"&gt;https://api.example.com/v1/orders&lt;/a&gt;&lt;br&gt;
Without re-running the request. clone() gives you a second readable stream:&lt;br&gt;
const text = await res.clone().text();&lt;br&gt;
console.log(res.status, res.headers.get("content-type"));&lt;br&gt;
console.log(text.slice(0, 200));&lt;br&gt;
In DevTools. Network panel, filter to Fetch/XHR, sort by status, open the failing row, read the Response tab. Not the Preview tab — Preview tries to render the body, and it will happily render a 404 page as if it were a document.&lt;br&gt;
The rule that prevents the next one&lt;br&gt;
Never call .json() on a response you have not inspected. Read the text, decide whether it is JSON, parse it yourself — so that when it fails, the error message contains evidence instead of a character.&lt;br&gt;
async function readJson(res) {&lt;br&gt;
  const text = await res.text();&lt;br&gt;
  const type = res.headers.get("content-type") ?? "";&lt;/p&gt;

&lt;p&gt;if (!type.includes("json")) {&lt;br&gt;
    throw new Error(&lt;br&gt;
      &lt;code&gt;${res.status} ${res.url} returned "${type || "no content-type"}":&lt;/code&gt; +&lt;br&gt;
        text.slice(0, 120),&lt;br&gt;
    );&lt;br&gt;
  }&lt;br&gt;
  return JSON.parse(text);&lt;br&gt;
}&lt;br&gt;
The part that matters is text.slice(0, 120). The next time this happens, your log contains &amp;lt;!DOCTYPE html&amp;gt;&lt;/p&gt;
404 or a Cloudflare banner or a login form. That is a cause. A character is not.&amp;lt;br&amp;gt;
Two smaller habits worth keeping:&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;Treat 204 No Content as success with no body. It is the correct response to a DELETE, so a client that always calls .json() fails on a successful request. That failure has a different message — Unexpected end of JSON input — because an empty string has no first character to be unexpected.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Strip a leading U+FEFF from anything that came from a file rather than the network. A byte-order mark is invisible in every editor, is not whitespace to a strict parser, and produces Unexpected token '﻿', "﻿{"a":1}" is not valid JSON — where the quoted character looks empty.
"At position 0" is legacy wording
I captured these by running each input through Node 22.22.2. None of them are paraphrased:
Engine
Message
Node 22 / current V8
Unexpected token '&amp;lt;', "&amp;lt;!DOCTYPE "... is not valid JSON
Older Chrome, Safari
Unexpected token &amp;lt; in JSON at position 0
Firefox
JSON.parse: unexpected character at line 1 column 1 of the JSON data
Any engine, empty body
Unexpected end of JSON input
Any engine, plain-text error
Unexpected token 'E', "Error: not found" is not valid JSON
Current V8 no longer says "at position 0" — it quotes the first ten characters instead. That is why half the Stack Overflow answers you will find describe a message your browser stopped printing a few versions ago. The behaviour is identical; only the wording moved.
Which leads to the practical rule: never parse the message. Catch the exception, treat it as "invalid", surface the text to a human. A regex against Unexpected token will break on the next runtime upgrade.
If the response came from your own server
Once you have the body, the cause is normally one of five, and every one of them is a routing or header problem rather than a JSON problem:&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;The catch-all is registered before the API route. app.get("*") in Express 4, app.get("/*splat") in Express 5, or a static handler above the router, swallows the request and serves the shell.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;The error handler returns HTML. A framework's default error page does exactly that. If the request accepted JSON, answer with JSON.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;The rewrite rule covers too much. /* → /index.html must exclude your API prefix.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Auth is handled by a redirect. A session guard answers an expired token with a 302 to the login page. An API should answer 401 and let the client decide.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;A proxy owns the error. If the body is stamped nginx or Cloudflare, your application never ran. Look at upstream health, not at code.
The one-line version
If the first byte of the body is { or [, it is JSON and the mistake is inside it. If it is &amp;lt;, it is a web page and the mistake is in your routing, your auth, or your base URL.
Everything else in this post is a consequence of that sentence.
I run a small set of browser-only JSON tools — formatter, validator, diff, minifier — and a longer version of this post, with the full table of engine differences, lives here: Unexpected token '&amp;lt;' in JSON at position 0. Nothing is uploaded; the parsing happens in the tab.&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;


</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>api</category>
      <category>json</category>
    </item>
    <item>
      <title>devto-post-1-precision</title>
      <dc:creator>xushoulin</dc:creator>
      <pubDate>Fri, 23 Oct 2026 06:04:21 +0000</pubDate>
      <link>https://dev.to/xushoulin/devto-post-1-precision-2j1j</link>
      <guid>https://dev.to/xushoulin/devto-post-1-precision-2j1j</guid>
      <description>&lt;p&gt;A support ticket I keep seeing, in various disguises: "the ID changed after we formatted the JSON." It didn't change because of the formatter. It changed the moment the document was parsed, before any tool could intervene.&lt;/p&gt;

&lt;p&gt;Here is the whole failure in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"id":9007199254740993}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="c1"&gt;// 9007199254740992&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing was malformed. The container was too small.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spec allows it; the runtime does not
&lt;/h2&gt;

&lt;p&gt;RFC 8259 does not put a size limit on numbers. It explicitly permits implementations to set limits on range and precision, and it warns that integers outside ±(2^53 − 1) "indicate potential interoperability problems."&lt;/p&gt;

&lt;p&gt;That sentence is the entire story. The format happily carries a 20-digit integer. The runtime almost everyone parses it with cannot hold one, because JavaScript has exactly one numeric type: a 64-bit float. Every number literal in a JSON document becomes a double before your code sees it.&lt;/p&gt;

&lt;p&gt;So the document is valid, the parser is correct, and the value is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the boundary actually is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt; is &lt;code&gt;9007199254740991&lt;/code&gt;, which is 2^53 − 1. An integer at or below that is represented exactly.&lt;/p&gt;

&lt;p&gt;The next integer, &lt;code&gt;9007199254740992&lt;/code&gt;, is 2^53 and is still exact — powers of two always are. But &lt;code&gt;9007199254740993&lt;/code&gt; does not exist as a double. It rounds &lt;strong&gt;down&lt;/strong&gt; to &lt;code&gt;9007199254740992&lt;/code&gt;, and so does every odd integer after it, until the next power of two where the spacing widens again.&lt;/p&gt;

&lt;p&gt;This is why the bug survives code review. An 18-digit snowflake ID sits comfortably inside the range of a 64-bit integer and comfortably outside the range of a double. The two ranges overlap just enough for everyone to assume they're the same range.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a round trip actually changes
&lt;/h2&gt;

&lt;p&gt;I ran the same document through &lt;code&gt;JSON.parse&lt;/code&gt; and then &lt;code&gt;JSON.stringify&lt;/code&gt; — which is exactly what "format" means — and recorded every difference.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;After round trip&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{"id":9007199254740993}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"id": 9007199254740992}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Numbers become IEEE-754 doubles. Integers above 2^53 lose their last digits.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{"b":1,"2":2,"1":3}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"1": 3, "2": 2, "b": 1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keys that look like array indices are enumerated first, ascending. That is a JavaScript object rule, not a JSON one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{"a":1,"a":2}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"a": 2}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Duplicate member names collapse silently: the last value wins and the others are gone.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{"x":1.0,"y":1e3,"z":-0}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"x": 1, "y": 1000, "z": 0}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Numbers are re-printed in their shortest round-trippable form.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"\u00e9\/"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"é/"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Escapes are decoded, then only the ones that must be escaped are written back.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only the first row is real information loss. The other four are normalisation: the bytes differ, the meaning does not.&lt;/p&gt;

&lt;p&gt;That distinction matters more than it looks. If you diff two documents that have both been through a formatter, you will get a report of changes no human made — reordered keys, &lt;code&gt;1.0&lt;/code&gt; becoming &lt;code&gt;1&lt;/code&gt;, an escaped slash becoming a plain one. I built a &lt;a href="https://www.shoulin.shop/json-diff" rel="noopener noreferrer"&gt;JSON diff tool&lt;/a&gt; around this, and the "ignore key order" toggle exists precisely because of row two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Object key order is not insertion order
&lt;/h2&gt;

&lt;p&gt;Row two is the one people rarely expect. A JavaScript object enumerates integer-like keys first, in ascending numeric order, &lt;em&gt;then&lt;/em&gt; the remaining string keys in insertion order.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{"b":1,"2":2,"1":3}&lt;/code&gt; therefore comes back as &lt;code&gt;{"1":3,"2":2,"b":1}&lt;/code&gt; — and no, that is not a bug in the formatter. JSON itself defines no order at all; it is a set of name/value pairs. Any consumer depending on member order was relying on undefined behaviour from the start.&lt;/p&gt;

&lt;p&gt;If order is part of your payload, put it in the data: an array of objects with an explicit &lt;code&gt;position&lt;/code&gt; field, not the order the bytes happened to arrive in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to keep big integers intact
&lt;/h2&gt;

&lt;p&gt;In order of how much you should prefer them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Make the field a string in the API.&lt;/strong&gt; This is what large platforms do. &lt;code&gt;{"id":"9007199254740993"}&lt;/code&gt; is unambiguous, survives every parser on every platform, and costs one pair of quotes. Nobody downstream has to remember a rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Intercept the parse with a reviver.&lt;/strong&gt; A reviver can inspect each value before your code sees it. Recent V8 engines pass a third argument that exposes the raw literal, which is what makes this reliable instead of guesswork:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// context.source is the literal exactly as written in the document.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/^-&lt;/span&gt;&lt;span class="se"&gt;?\d&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I verified this on Node 22.22.2 — &lt;code&gt;context.source&lt;/code&gt; really does hold &lt;code&gt;"9007199254740993"&lt;/code&gt; before the double conversion wins. But feature-detect it rather than trusting it: if &lt;code&gt;context&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;, the engine predates the feature and the digits are already gone. In that case fall back to option 1 or 3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Use a lossless parser.&lt;/strong&gt; Libraries like &lt;code&gt;lossless-json&lt;/code&gt; keep the original text of each number and hand you a wrapper type. They work everywhere, at the cost of a dependency and a value type nobody on your team has seen before.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you hash or sign JSON
&lt;/h2&gt;

&lt;p&gt;A round trip is not byte-stable, so &lt;strong&gt;never re-serialise a payload that was signed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two documents can be semantically identical and hash differently because of whitespace, key order, or number formatting — &lt;code&gt;1.0&lt;/code&gt; and &lt;code&gt;1&lt;/code&gt; are the same float and different bytes. A payload that went through &lt;code&gt;JSON.parse&lt;/code&gt; and back may no longer match its own signature, and the failure looks like a corrupted message rather than a formatting difference.&lt;/p&gt;

&lt;p&gt;Sign the exact bytes you send: serialise once, hash that string, transmit the same string. On the receiving side, verify before parsing, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;JSON allows numbers of any length; JavaScript has one numeric type, and it's a float.&lt;/li&gt;
&lt;li&gt;The safe range is ±(2^53 − 1). Snowflake IDs sit just outside it, which is why this bites production systems and not tutorials.&lt;/li&gt;
&lt;li&gt;Strings for IDs. Always.&lt;/li&gt;
&lt;li&gt;A formatter can't fix this, because the damage happens inside the parse. Any tool claiming to preserve &lt;code&gt;9007199254740993&lt;/code&gt; as a number is lying about what the runtime holds afterwards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full version, with the same measurements and a few more cases, is at &lt;a href="https://www.shoulin.shop/guides/why-json-numbers-lose-precision" rel="noopener noreferrer"&gt;shoulin.shop/guides/why-json-numbers-lose-precision&lt;/a&gt;. If you want to watch the loss happen to your own payload, &lt;a href="https://www.shoulin.shop/json-formatter" rel="noopener noreferrer"&gt;the formatter&lt;/a&gt; runs entirely in the tab — no upload, no account, and the Network tab will show you nothing leaving.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
