TypeError: Do not know how to serialize a BigInt

Quick answer

JSON.stringify throws on a BigInt because JSON has no integer type that can hold values beyond Number.MAX_SAFE_INTEGER without losing precision. Convert the BigInt to a string with a replacer (typeof v === 'bigint' ? v.toString() : v), and convert it back with a matching reviver on JSON.parse.

The exact error string

const payload = { id: 9007199254740993n, name: 'order' };

JSON.stringify(payload);
// Uncaught TypeError: Do not know how to serialize a BigInt
//     at JSON.stringify (<anonymous>)

Unlike undefined or a function — which JSON.stringify silently drops — a BigInt throws a hard TypeError. That is deliberate: a BigInt holds a value that would be corrupted if written as a JSON number, so the engine refuses rather than lose data quietly.

Why JSON.stringify refuses a BigInt

JSON numbers are IEEE-754 double-precision floats. They can represent integers exactly only up to Number.MAX_SAFE_INTEGER, which is 2^53 - 1 (9007199254740991). BigInt was added to JavaScript specifically to hold integers larger than that — database IDs, Twitter/X snowflake IDs, blockchain values, nanosecond timestamps.

If JSON.stringify wrote a BigInt as a plain JSON number, the moment any standard parser read it back with JSON.parse it would become a Number and silently lose its lowest digits. To avoid that trap, the spec makes BigInt serialization an explicit error you must handle — there is no lossless standard JSON number for it.

// Why "just use a number" fails:
const big = 9007199254740993n;          // one past MAX_SAFE_INTEGER
Number(big);        // => 9007199254740992   ← wrong, lost the last digit
JSON.parse('9007199254740993');         // => 9007199254740992   ← same loss

Fix 1: convert to a string with a replacer (recommended)

Pass a replacer as the second argument to JSON.stringify. It runs for every value and lets you turn each BigInt into its exact string form:

const payload = { id: 9007199254740993n, name: 'order' };

const json = JSON.stringify(payload, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// => '{"id":"9007199254740993","name":"order"}'

The value is preserved exactly as text. The trade-off — and the part to be honest about — is that id comes back as a string, not a BigInt, when someone parses this JSON. If the consumer needs the BigInt type again, it must convert explicitly (Fix 3).

Fix 2: BigInt.prototype.toJSON (convenient, with a caveat)

If a value has a toJSON() method, JSON.stringify calls it automatically. Defining one on BigInt.prototype makes every BigInt serialize as a string without a replacer:

// Run once at startup:
BigInt.prototype.toJSON = function () { return this.toString(); };

JSON.stringify({ id: 9007199254740993n });
// => '{"id":"9007199254740993"}'   — no replacer needed

Caveat: this mutates a built-in prototype globally. Every BigInt in your process — including ones inside third-party libraries — now serializes as a string, which can surprise code that expected the original throw. Use it only in an application you fully control; never ship it in a shared library. In most codebases the local replacer (Fix 1) is the safer choice.

Fix 3: the round-trip problem — parse it back

A JSON string carries no type information, so "9007199254740993" is just text when it comes back. To restore the BigInt, use a reviver with JSON.parse — but you must know which keys are BigInt:

const text = '{"id":"9007199254740993","name":"order"}';

const data = JSON.parse(text, (key, value) =>
  key === 'id' ? BigInt(value) : value
);
data.id;            // => 9007199254740993n   ← BigInt restored
typeof data.id;     // => 'bigint'

Fix 4: a self-describing wrapper (no key list needed)

If you do not want to hard-code key names, tag each BigInt with a wrapper object on the way out, and detect that shape on the way in. This makes the value self-describing and round-trips cleanly regardless of where it sits:

// Serialize: wrap every BigInt
const json = JSON.stringify(payload, (k, v) =>
  typeof v === 'bigint' ? { $bigint: v.toString() } : v
);

// Parse: unwrap anything tagged $bigint
const data = JSON.parse(json, (k, v) =>
  v && typeof v === 'object' && typeof v.$bigint === 'string'
    ? BigInt(v.$bigint)
    : v
);

Be honest with yourself about the cost: this changes the wire format (consumers see {"$bigint":"..."}), so it only works when both ends agree on the convention. There is no lossless, universally understood JSON encoding for BigInt — every option trades something.

Where BigInt values come from

You rarely type 123n by hand — BigInt usually arrives from somewhere that needs the extra range, which is why the error appears deep in real apps:

The parse side: JSON.parse loses precision too

This is the trap on the way in, and it is worse than the stringify error because it is silent. If the JSON text contains a large number as a bare number (not a string), JSON.parse reads it as a Number and the value is already corrupted before you can convert it — there is no BigInt to recover:

const text = '{"id": 9007199254740993}';   // a 64-bit id as a JSON number

const data = JSON.parse(text);
data.id;            // => 9007199254740992   ← already wrong, last digit lost
BigInt(data.id);    // => 9007199254740992n  ← too late, precision is gone

The only lossless fix is to keep large integers as JSON strings end to end ("id": "9007199254740993") so the digits survive the parse, then convert with BigInt(value). Control the producer if you can — ask the API or set the DB driver to emit big integers as strings.

Libraries that handle the round trip

If you do not control the producer and the wire format already uses bare large numbers, a custom parser that reads numbers as BigInt is the only way to avoid the silent loss. Two well-known options:

Be honest about the trade-off: these change the types your whole codebase sees and add a dependency. When you own both ends, the string convention (above) is simpler and dependency-free. Typing these IDs as string in your interfaces — which you can generate from a sample with JSON to TypeScript — documents the decision and stops someone "fixing" it back to number.

Debugging checklist

Frequently Asked Questions

What does 'Do not know how to serialize a BigInt' mean?

It means you passed a BigInt value to JSON.stringify, which throws a TypeError because JSON has no type that can represent a BigInt. Unlike undefined or functions, which are silently dropped, a BigInt causes a hard error so you cannot accidentally lose a large number.

Why can't JSON.stringify handle BigInt?

JSON numbers are IEEE-754 doubles, which lose precision beyond Number.MAX_SAFE_INTEGER (2^53 - 1). BigInt exists precisely to hold integers larger than that, so writing it as a JSON number would silently corrupt the value. Rather than lose precision, JSON.stringify refuses and throws.

How do I serialize a BigInt to JSON?

Convert it to a string. Pass a replacer to JSON.stringify that returns value.toString() for any BigInt, for example JSON.stringify(data, (k, v) => typeof v === 'bigint' ? v.toString() : v). The value is preserved exactly as text; the trade-off is that it deserializes back as a string, not a BigInt, unless you use a matching reviver.

How do I parse a BigInt back from JSON?

Use a reviver with JSON.parse that turns the known fields back into BigInt: JSON.parse(text, (k, v) => k === 'id' ? BigInt(v) : v). Because a plain JSON string carries no type information, you must know which keys are BigInt, or tag them with a wrapper object like {"$bigint": "123"}.

Is it safe to add BigInt.prototype.toJSON globally?

It works — defining BigInt.prototype.toJSON = function () { return this.toString(); } makes JSON.stringify emit BigInt as a string everywhere — but it mutates a built-in prototype globally, which can surprise other code and libraries. Prefer a local replacer in application code; only patch the prototype in a controlled environment you fully own.

Should I serialize a BigInt as a string or a number?

Use a string. Emitting it as a raw JSON number re-introduces the exact precision loss BigInt was meant to avoid the moment any standard JSON.parse reads it back as a Number. A string keeps the value lossless; the cost is an explicit conversion on both ends. There is no lossless standard JSON number for BigInt.

Working with large-number JSON?

Format and validate any JSON payload in your browser — nothing is uploaded to a server.

JSON Formatter JSON to TypeScript All Error References
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.