Email me

Guide · 5 min read

Filed under App development Guides

How to handle 64-bit IDs in JSON with JavaScript BigInt

Steps
6
Hands-on
15 min
Reading
5 min
Checked
Oct 24

The short version

JavaScript numbers are 64-bit floats, so they hold integers exactly only up to 9,007,199,254,740,991 (2^53 - 1). JSON.parse turns every bigger integer into the nearest float without an error: in our test the ID 175928847299117063 came back as 175928847299117060. Fix it by sending IDs as strings, or by parsing with a reviver that reads ctx.source into a BigInt; when you send it back, JSON.stringify throws on BigInt unless you convert it first.

Diagram titled The same JSON, parsed two ways. The server sends id 175928847299117063. Plain JSON.parse returns 175928847299117060, last digit wrong, marked red: rounded to the nearest double, off by 3, no error. A reviver using ctx.source and BigInt returns 175928847299117063n, marked green: the original digits kept exactly.
One ID, two results. Plain JSON.parse turns the last digits into whatever the nearest 64-bit float happens to be, without an error. Output from our run of demo.js in Node.js 22.16, Oct 24 2026. Diagram by Tensorust from our own test run.

What you need

  • A JSON API or file with integer IDs larger than 16 digits (Snowflake, Twitter/X, Discord, Postgres bigint)
  • Node.js 22+ or a 2025-or-later browser for the built-in fix; any Node.js version for the library fix

Part of the article Pick a UUIDv7 library that keeps order: 10 tested, .NET fails 50%. This page answers one question from it in depth.

On this page · 12 sections
  1. Key numbers
  2. Check your JSON right here
  3. Before you start: why this happens
  4. Check whether your IDs are unsafe
  5. Fix it at the source: send IDs as strings
  6. If you cannot change the server, parse with a reviver
  7. On older runtimes, use a library
  8. Send BigInts back without a TypeError
  9. Test the round trip
  10. What goes wrong
  11. Related
  12. Sources

Every snippet below was run on October 24, 2026 in Node.js 22.16.0; the outputs shown are ours.

Key numbers

What Value Source (checked Oct 24 2026)
Largest exact integer in a JS number 9,007,199,254,740,991 (16 digits) MDN, Number.MAX_SAFE_INTEGER
A Discord-style Snowflake ID 175928847299117063 (18 digits) Discord API docs
What JSON.parse returns for it 175928847299117060 (off by 3) our run
JSON.stringify on a BigInt TypeError: Do not know how to serialize a BigInt our run
ctx.source in revivers, JSON.rawJSON TC39 Stage 4; MDN "Baseline 2025" TC39 proposal, MDN
json-bigint downloads, week of Oct 16-22 2026 37,213,334 npm API
lossless-json downloads, same week 1,232,694 npm API

Check your JSON right here

Numbers that JSON.parse would change show up here. Nothing leaves your browser.

Before you start: why this happens

JSON itself has no size limit on numbers. The problem is JavaScript: a number is an IEEE 754 double with 53 bits of precision, so past 2^53 it can only represent every second integer, then every fourth, and so on. Snowflake IDs (Twitter/X, Discord, Instagram-style), Postgres bigint keys and many payment and order IDs use up to 63 bits, far past that line. Our Snowflake ID vs UUID explainer shows the bit layout.

Nothing warns you. The rounded ID looks like a normal number, so the bug shows up later as "user not found" or the wrong record being updated.

Step 01: Check whether your IDs are unsafe

Number.MAX_SAFE_INTEGER                        // 9007199254740991
Number.isSafeInteger(JSON.parse('175928847299117063'))  // false

Rule of thumb: any integer with 17 or more digits is unsafe; 16-digit ones are unsafe above 9,007,199,254,740,991. Paste a real response into the checker above to see exactly which fields break.

Step 02: Fix it at the source: send IDs as strings

If you own the API, this is the fix that works for every client, in every language, forever. Discord's API documents that it returns every Snowflake as a string for this reason, and X's API pairs id with an id_str field (X docs).


json.dumps({"id": str(row.id)})          # {"id": "175928847299117063"}
// Go: the ,string option writes and reads the int64 as a JSON string
type User struct {
    ID int64 `json:"id,string"`
}
-- Postgres: cast in the query that builds the JSON
SELECT json_build_object('id', id::text) FROM users;

Keep the column a 64-bit integer in the database; only the JSON gets the string. The Go encoding/json docs describe the string option.

Step 03: If you cannot change the server, parse with a reviver

Since the "JSON.parse source text access" proposal reached Stage 4 at TC39 (champions Richard Gibson and Mathias Bynens), revivers get a third argument, ctx, whose source is the original text of each primitive. That text still has every digit:

const data = JSON.parse(body, (key, value, ctx) =>
  key === 'id' ? BigInt(ctx.source) : value);
data.id   // 175928847299117063n

Only convert the fields you know are IDs. Converting every number to BigInt breaks arithmetic with normal numbers (1n + 1 throws a TypeError).

Terminal capture of demo.js and its output in Node.js v22.16.0. Plain JSON.parse prints 175928847299117060 (safe? false); the reviver prints 175928847299117063n; JSON.stringify of the BigInt throws TypeError: Do not know how to serialize a BigInt; the string replacer prints {"id":"175928847299117063"}; the rawJSON replacer prints {"id":175928847299117063}.
All four behaviours from this guide in one run: the silent rounding, the reviver fix, the TypeError on the way out and both ways to avoid it. The script is demo.js on this page's sources list. Captured from our terminal, October 24, 2026.

Step 04: On older runtimes, use a library

ctx.source is missing in Node.js 20 and earlier and in browsers from before 2025; there the third argument is undefined. Two maintained parsers read big integers without going through a float:

// npm i json-bigint
const JSONbig = require('json-bigint')({ useNativeBigInt: true });
JSONbig.parse('{"id":175928847299117063,"n":5}');
// { id: 175928847299117063n, n: 5 }   small numbers stay numbers

json-bigint (by Andrey Sidorov) had 37.2 million npm downloads in the week of October 16 to 22, 2026; lossless-json (by Jos de Jong) had 1.2 million and wraps numbers in a LosslessNumber class instead.

Step 05: Send BigInts back without a TypeError

JSON.stringify({ id: 1n }) throws TypeError: Do not know how to serialize a BigInt. Pass a replacer:

// As a string (what most APIs want):
JSON.stringify(data, (k, v) => typeof v === 'bigint' ? v.toString() : v);
// {"id":"175928847299117063"}

// As a bare JSON number, exact (Node.js 22+, Baseline 2025 browsers):
JSON.stringify(data, (k, v) => typeof v === 'bigint' ? JSON.rawJSON(v.toString()) : v);
// {"id":175928847299117063}
Screenshot of the MDN page for JSON.rawJSON showing the badge Baseline 2025, Newly available, with Chrome, Edge, Firefox and Safari ticked, and the description: The JSON.rawJSON static method creates a raw JSON object containing a piece of JSON text.
MDN marks JSON.rawJSON as Baseline 2025: it works in current Chrome, Edge, Firefox and Safari, but not in older browsers. Screenshot: developer.mozilla.org, captured October 24, 2026.

Step 06: Test the round trip

const raw = '{"id":175928847299117063}';
const out = JSON.stringify(parse(raw), replacer);
assert.equal(out, '{"id":"175928847299117063"}'); // or the raw form

Use an ID above 2^53 in the fixture. Tests with small IDs like 42 pass even when the bug is there, which is how this ships to production.

Troubleshooting

What goes wrong

  • Sorting string IDs. ['99', '100'].sort() gives ['100', '99']. Sort with (a, b) => (BigInt(a) < BigInt(b) ? -1 : 1), or compare length first.
  • Number(id) in the UI. A string ID that goes through Number(), parseInt() or a numeric input field gets rounded all over again.
  • Patching BigInt.prototype.toJSON. It works, and MDN shows it, but it changes every JSON.stringify in the whole process, including third-party code. Prefer a replacer.
  • json-bigint's string mode. With storeAsString: true it returns objects with a null prototype, so obj.hasOwnProperty is missing. Use Object.hasOwn(obj, key).
  • TypeScript types. Typing an ID as number invites the bug back. Use string, or bigint after Step 3.

Sources

All checked October 24, 2026.

Specs and docs

Libraries

Our tests

  • demo.js: the script in the terminal capture, run with Node.js v22.16.0 on Windows 11, October 24, 2026. json-bigint 1.0.0 tested in the same Node.js version. The Python snippet was run on Python 3.12; the Go and SQL snippets follow the linked docs and were not run by us.

Images

  • Hero diagram and terminal capture made by Tensorust from our own run. MDN screenshot: developer.mozilla.org, captured October 24, 2026.

Do this next

Every number on this page links to its source and was checked on Oct 24, 2026. Spot an error or a newer figure? Email matt@tensorust.com and it gets fixed. Related: Snowflake ID decoder, UUIDv7 generator and decoder.

Read next

Back to The Manual →   All articles →

Get the next one by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Up next

The article this answer comes fromPick a UUIDv7 library that keeps order: 10 tested, .NET fails 50%We tested 10 UUIDv7 libraries with 1M IDs each and a clock jump. .NET put 50% out of order, uuid6 ran 16.6 min ahead. The 2 that passed, plus free data.Read it next →

Get the receipts by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Did this answer your question?

Popular in the shop

All 124 tools →

Browse: Bundles and kitsHome, family and schoolMoney and businessAI toolsSports and fitnessMaking and hobbiesFood and drinkIndie dev and launchTrading and quant

Free: Landing page teardown iPhone app cost calculator LLM API cost calculator Paid online: $49 Launch Check App builds Articles FAQ