TypeError: Do Not Know How to Serialize a BigInt — Causes and Fixes

Quick answer

💡BigInt values throw 'TypeError: Do not know how to serialize a BigInt' because JSON has no BigInt type — its numbers map to IEEE 754 doubles, capped at 2^53-1. Pass a replacer to JSON.stringify: (key, value) => typeof value === 'bigint' ? value.toString() : value. Receivers must parse those string fields back as BigInt explicitly.

Error symptoms

  • TypeError: Do not know how to serialize a BigInt
  • JSON.stringify throws synchronously when any value in the object is typeof bigint
  • fetch body serialization fails when BigInt is inside a nested property
  • Numbers above 9007199254740991 round to the nearest double when parsed back
  • Twitter snowflake IDs change value after a round-trip through JSON.parse
  • PostgreSQL bigint columns return values that fail equality checks after JSON serialization

Common causes

  • Reading BigInt columns from PostgreSQL or MySQL drivers that return native BigInt
  • Using the BigInt() constructor or numeric literals with the n suffix (42n) in data objects
  • Snowflake IDs or Twitter IDs stored as 64-bit integers exceeding Number.MAX_SAFE_INTEGER
  • Calling res.json() in Express with an object containing BigInt fields from a database query
  • Third-party libraries that return BigInt for timestamp or ID fields
  • Node.js crypto functions returning BigInt for modular arithmetic results

When it happens

  • When passing a database row directly to JSON.stringify after a query that returns bigint columns
  • When building a REST API endpoint that serializes objects containing snowflake-format IDs
  • When a logging library tries to serialize request or response bodies that include BigInt values
  • When a GraphQL resolver returns BigInt scalars that the serialization layer converts to JSON
  • When structured cloning or worker message passing involves objects that later get serialized

Examples and fixes

A PostgreSQL query returns a row where the id column is a native BigInt. Passing it directly to JSON.stringify throws immediately.

Serializing a database row with a BigInt primary key

❌ Wrong

const { rows } = await pool.query(
  'SELECT id, username, created_at FROM users WHERE id = $1',
  [userId]
);
const userRow = rows[0];
// userRow.id is 9223372036854775807n (BigInt from pg driver)
console.log(typeof userRow.id); // 'bigint'
const body = JSON.stringify(userRow);
// ^ TypeError: Do not know how to serialize a BigInt
res.send(body);

✅ Fixed

const { rows } = await pool.query(
  'SELECT id, username, created_at FROM users WHERE id = $1',
  [userId]
);
const userRow = rows[0];

const bigintReplacer = (key, value) =>
  typeof value === 'bigint' ? value.toString() : value;

const body = JSON.stringify(userRow, bigintReplacer);
// { "id": "9223372036854775807", "username": "alice", ... }
res.setHeader('Content-Type', 'application/json');
res.send(body);

The PostgreSQL node driver returns bigint columns as native JavaScript BigInt values starting from certain versions. JSON.stringify has no built-in handler for BigInt and throws immediately rather than producing incorrect output. The replacer function intercepts every value during serialization and converts BigInt to its string representation before JSON.stringify tries to encode it. The string form preserves all digits exactly, unlike casting to Number which loses precision above 2^53-1. The API consumer must know that the id field is a numeric string and parse it with BigInt() or a lossless library when needed.

A snowflake ID from a social platform exceeds Number.MAX_SAFE_INTEGER. Using json-bigint instead of the built-in JSON object preserves full precision on both ends.

Preserving snowflake IDs through a JSON round-trip

❌ Wrong

// Built-in JSON cannot handle BigInt at all
const tweetData = {
  tweetId: 1234567890123456789n,
  authorId: 987654321098765432n,
  text: 'Hello from the API',
  likeCount: 4821n
};

// This line throws:
const serialized = JSON.stringify(tweetData);
// TypeError: Do not know how to serialize a BigInt
console.log(serialized);

✅ Fixed

import JSONBig from 'json-bigint';

const tweetData = {
  tweetId: 1234567890123456789n,
  authorId: 987654321098765432n,
  text: 'Hello from the API',
  likeCount: 4821n
};

// json-bigint serializes BigInt as unquoted large numbers
const serialized = JSONBig.stringify(tweetData);
// {"tweetId":1234567890123456789,"authorId":987654321098765432,...}

// Parsing back yields BigInt, not a rounded Number
const parsed = JSONBig.parse(serialized);
console.log(parsed.tweetId === tweetData.tweetId); // true

The json-bigint package replaces both JSON.stringify and JSON.parse with implementations that treat numbers outside the safe integer range as BigInt automatically. The wire format does not quote the IDs — they appear as plain JSON numbers — which is compatible with platforms like Java or Go that can parse large integers natively. The key advantage over the replacer-to-string approach is that consumers using languages with 64-bit integer support parse the numbers correctly without special handling. Choose json-bigint when your API serves multiple client languages, and choose the string approach when you need the ID to be unambiguously a string type in the JSON schema.

IEEE 754 doubles and the 2-to-the-53 boundary

JavaScript's Number type follows the IEEE 754 double-precision floating-point standard. A 64-bit double uses one bit for the sign, eleven bits for the exponent, and fifty-two bits for the fractional mantissa. Because of how the implicit leading 1 bit works, this gives exactly 53 bits of integer precision, meaning every integer from negative 2^53 to positive 2^53 can be represented without any rounding. The constant Number.MAX_SAFE_INTEGER, which equals 9007199254740991, marks the boundary. Beyond it, consecutive integers collapse into the same floating-point value, so 9007199254740992 and 9007199254740993 are indistinguishable as Number.

The JSON specification, defined in RFC 8259, does not distinguish between integers and floating-point numbers. A JSON number is simply a sequence of digits, optionally with a decimal point and exponent. There is no BigInt, int64, or arbitrary-precision type in the JSON grammar. When a JSON parser reads a large number, it must decide what JavaScript type to use, and the default choice is Number. This means any integer above 2^53-1 gets silently rounded during parsing, even if it was serialized correctly.

The BigInt primitive type was introduced in JavaScript to solve this. A BigInt can represent integers of arbitrary size with no loss of precision. However, because there is no corresponding type in JSON, BigInt.prototype.toJSON is undefined. When JSON.stringify encounters a BigInt value, it cannot produce a standard-compliant representation — it cannot write a quoted string without changing the semantic type, and it cannot write an unquoted number without risking precision loss on the receiving end. Rather than silently corrupt data, the specification mandates that stringify throw a TypeError.

This is a deliberate design decision: silent data corruption when serializing database IDs or financial amounts would be far more harmful than an explicit error. The TypeError forces developers to make a conscious choice about the representation — string, lossless number library, or application-level encoding — rather than discovering precision loss in production when two IDs collide after rounding.

Catching BigInt serialization before runtime

The TypeError thrown by JSON.stringify is synchronous and will crash the current request handler or execution context if not caught. In Express, an uncaught synchronous error in a route handler propagates to the default error handler, which sends a 500 response. The stack trace points to the JSON.stringify call, but the root cause is typically several frames up where the BigInt value was created or returned from a database query.

Start diagnosis by logging the typeof each field in the object before serialization. A quick Object.entries(obj).map(([k, v]) => [k, typeof v]) will reveal which field holds a bigint. If the object is nested, write a small recursive inspector or use JSON.stringify with a replacer that logs the key and type before returning, so you can see exactly which path triggers the error.

TypeScript can help catch this at compile time. If your database ORM or query result type is typed with bigint fields, the TypeScript compiler will surface a type error when you try to pass that object to a function that expects a JSON-serializable type. Define a JsonSafe utility type that maps bigint to string and use it as the return type of your serialization helpers. This way, the mismatch between the database model and the JSON contract becomes a compile-time failure rather than a runtime crash.

For existing codebases, audit all calls to JSON.stringify, Express's res.json, and any logging statements that accept entire request or response objects. Pay special attention to database query results, especially from PostgreSQL when using the pg driver with bigint columns, MongoDB ObjectIds, or any library that follows the TC39 BigInt specification. Add a try-catch around JSON.stringify during the audit phase and log the full object when it throws to identify all serialization paths that need fixing.

Replacer functions for BigInt-to-string conversion

The most portable fix is a replacer function passed as the second argument to JSON.stringify. The replacer receives each key-value pair and can return a transformed value or leave it unchanged. For BigInt, return value.toString() to produce a quoted numeric string in the JSON output. This approach requires no additional dependencies and works in any JavaScript environment that supports BigInt.

You can define a module-level replacer and import it wherever serialization happens. A function like const bigintReplacer = (key, value) => typeof value === 'bigint' ? value.toString() : value is small, testable, and composable with other replacers. If you already have a replacer that handles other types such as Date or Map, add the BigInt branch alongside them without conflict.

For Node.js HTTP servers, Express's res.json() calls JSON.stringify internally and does not expose a replacer argument. Instead, serialize the object manually with JSON.stringify(data, bigintReplacer) and send it with res.send(body) after setting the Content-Type header to application/json. This gives you full control over the serialization pipeline without wrapping or monkey-patching Express.

Another approach is to define toJSON on BigInt.prototype directly: Object.defineProperty(BigInt.prototype, 'toJSON', { value: function() { return this.toString(); }, writable: true, configurable: true }). JSON.stringify checks for a toJSON method on each value before applying the replacer, so this makes every BigInt serialize to its string form automatically throughout the process. The downside is that it mutates a built-in prototype, which is generally discouraged because it can conflict with other libraries or future language changes. Reserve it for application entry points, never for shared libraries.

For full lossless round-trips, the lossless-json npm package provides both stringify and parse that preserve all number precision using a special LosslessNumber type internally. Unlike json-bigint, lossless-json does not commit to BigInt specifically — it keeps the original string representation and lets you convert to whatever type you need during processing, which is useful when the same payload mixes normal numbers with large integers.

String encoding versus native number for IDs

Choosing between sending a large integer as a JSON string or as an unquoted JSON number has real consequences for interoperability. When you serialize a BigInt as a string — producing { "userId": "9223372036854775807" } — the JSON type is unambiguously a string. Every consumer must explicitly convert it. JavaScript clients call BigInt('9223372036854775807'), Python calls int('9223372036854775807'), and Java calls Long.parseLong. The advantage is that no consumer can accidentally treat it as a floating-point number and lose precision during parsing.

When you use json-bigint or a similar library to emit the number as an unquoted integer — producing { "userId": 9223372036854775807 } — the JSON is technically a number token. JavaScript's standard JSON.parse rounds it because Number cannot hold it exactly. Go, Rust, Java, and Python all parse it correctly as a 64-bit or arbitrary-precision integer. This format is natural for strongly-typed languages but dangerous for JavaScript consumers that forget to use a lossless parser.

Twitter learned this lesson with its snowflake IDs. The public API sends the id field as both a native JSON number and a separate id_str string field, specifically because early JavaScript clients were losing precision when parsing the native number form. The official recommendation for JavaScript clients was always to use id_str, not id. This dual-field pattern adds payload size but guarantees that all client implementations can access the correct value regardless of their JSON parser.

A pragmatic schema convention is to use string IDs for any identifier that could conceivably exceed 2^53. UUIDs are already strings. Snowflake IDs, database bigint primary keys, and cryptographic identifiers should be strings in your JSON schema by design, not as a workaround. This eliminates the entire class of precision-loss bugs at the schema layer rather than requiring every client to use a special parser.

Twitter snowflake IDs losing precision in browsers

The most commonly reported real-world instance of BigInt precision loss is the Twitter snowflake ID format. A snowflake ID is a 64-bit integer composed of a millisecond timestamp, a machine ID, and a sequence number. IDs issued in 2023 and later are well above one quadrillion, which is orders of magnitude above Number.MAX_SAFE_INTEGER. When a browser fetches a timeline and JSON.parse runs on the response, every tweet ID that arrives as a plain JSON number is rounded to the nearest representable double.

The rounding is subtle and dangerous. The actual integer 1616458413511311361 and the actual integer 1616458413511311360 both map to the same double-precision float value. If the browser stores this rounded value and later sends it back to the API — for example, as a cursor parameter for the next page of results — the server receives an ID that no longer exists in the database. This produces either a 404, the wrong page of results, or silent data skipping with no error visible to the user.

In browser environments, there is no BigInt JSON parsing built in. The fetch API uses the standard JSON.parse, which always rounds large numbers. The only remedies are to use a lossless JSON library on the client side when bundling a JavaScript application, to request that the API return large IDs as strings, or to use the Response.text() method to get the raw response body as a string and process it with a streaming parser before numbers are converted to JavaScript values.

The same progressive failure pattern appears with database sequence IDs that grow over time. A table that starts with small IDs will behave correctly for years until the counter crosses 2^53, at which point subtly wrong behavior begins appearing in clients that rely on JavaScript's native number type. Audit your schema now for any identifier column typed as bigint and ensure the API contract uses string representation before that threshold is reached.

Schema contracts that prevent BigInt precision loss

Preventing BigInt serialization errors is largely a matter of establishing clear conventions at the schema layer before they become runtime bugs. Define a rule in your API documentation and TypeScript types: any identifier, sequence number, or count that could exceed 9007199254740991 must be serialized as a string. Make this the default for all database primary keys typed as bigint, not an exception added after a bug report.

In OpenAPI or JSON Schema, declare these fields with type: string and a format annotation like int64-string or numeric-string. This communicates intent to consumers and allows validators to confirm that the field contains only digits. Do not use type: number for any field that comes from a database bigint column, even if current values happen to be small.

Add a serialization test to your CI pipeline that generates an object with realistic BigInt values — values that actually match what your database would return at scale — and verifies that JSON.stringify with your replacer produces the expected string output. Then add a corresponding parse test that reads that JSON string and reconstructs the BigInt values correctly. These two tests together form a regression gate against precision loss bugs that would otherwise only appear in production.

For teams using Zod or other runtime schema validators, define a bigintAsString schema helper that combines z.bigint().transform(v => v.toString()) for output and z.string().regex(/^\d+$/).transform(v => BigInt(v)) for input. This makes the conversion explicit in the type system and ensures it is applied consistently across all serialization paths. Treat the conversion as a codec boundary — BigInt inside the application, string on the wire — and enforce it at every API endpoint rather than at individual call sites.

Quick fix checklist

  • Identify which fields are typeof bigint by logging Object.entries(obj).map(([k, v]) => [k, typeof v])
  • Add a replacer function: (key, value) => typeof value === 'bigint' ? value.toString() : value
  • Pass the replacer as the second argument to every JSON.stringify call touching BigInt data
  • Or use json-bigint or lossless-json for full round-trip precision without string conversion
  • Update your JSON schema to declare large-integer fields as type string with a numeric-string format
  • Add TypeScript types that map bigint database fields to string in the API response shape
  • Write a round-trip test: serialize a BigInt object and verify the parsed result matches the original
  • Audit Express res.json() calls that pass database query results containing bigint columns

Related guides

Frequently asked questions

Why does JSON.stringify throw for BigInt but not for other unsupported types?

JSON.stringify silently converts undefined, functions, and symbols to undefined — dropping object keys or replacing array slots with null. The spec authors decided BigInt should throw instead because silently dropping or truncating a large integer would corrupt financial amounts or database IDs invisibly. A TypeError forces the developer to make an explicit serialization choice rather than discovering data corruption in production.

Can I use JSON.stringify with BigInt by setting BigInt.prototype.toJSON?

Yes. Setting Object.defineProperty(BigInt.prototype, 'toJSON', { value: function() { return this.toString(); } }) makes JSON.stringify call that method for every BigInt value, producing a quoted string in the output. It works, but mutating built-in prototypes is generally discouraged in library code. It is acceptable in application code when you control the entire JavaScript environment and no third-party library depends on BigInt.prototype.toJSON being undefined.

Will json-bigint produce valid JSON that standard parsers can read?

The output of JSONBig.stringify is valid JSON — it produces standard number tokens for large integers. The problem arises on the parsing side: standard JSON.parse in JavaScript rounds those large numbers to the nearest double. Go, Java, Python, and Rust all parse the numbers correctly as 64-bit or arbitrary-precision integers. JavaScript clients must also use JSONBig.parse to avoid precision loss during deserialization.

What is Number.MAX_SAFE_INTEGER and why does it matter for JSON?

Number.MAX_SAFE_INTEGER is 9007199254740991, which equals 2^53 minus one. Any integer up to this value can be represented exactly as an IEEE 754 double. Above it, consecutive integers share the same floating-point representation. JSON parsers that decode numbers into JavaScript's Number type silently lose precision for integers above this threshold, causing IDs and counts to change value without any error or warning.

Do PostgreSQL drivers always return BigInt for bigint columns?

It depends on the driver and its configuration. The pg (node-postgres) driver returns bigint columns as JavaScript strings by default to avoid precision loss. Some newer drivers or explicit configurations may return native BigInt. Always check your driver documentation and test by logging typeof on a returned bigint column value. If it returns a string, no special serialization is needed, but you must parse it as BigInt before arithmetic.

How do I handle BigInt in JSON API responses in Express?

Express's res.json() calls JSON.stringify internally and does not accept a replacer. Serialize the object manually with JSON.stringify(data, bigintReplacer) and send the result with res.send(body) after setting the Content-Type header to application/json. Alternatively, define BigInt.prototype.toJSON globally at application startup, which applies to every serialization call in the process without modifying each route.

Is there a way to detect BigInt values in a nested object before calling stringify?

Write a recursive checker that uses typeof value === 'bigint' and walks nested objects and arrays. For large or deeply nested structures, a non-recursive approach using a work queue is safer. Some schema validation libraries like Zod also surface type mismatches if you define your schema with z.bigint() fields and run validation before serialization, letting you catch BigInt values before stringify is called.

What happens to BigInt values in arrays during JSON.stringify?

JSON.stringify throws the same TypeError for BigInt values in arrays as it does for BigInt values in object properties. There is no special handling for arrays. The replacer function receives each element with its numeric index as the key, and the same typeof value === 'bigint' check applies. A correctly written replacer handles both object properties and array elements with no additional code.

Should I use string IDs everywhere to avoid this problem entirely?

Many modern APIs do exactly that. UUIDs are already strings, and string snowflake IDs are common practice. The cost is that you cannot do numeric comparisons directly in JavaScript — you must parse the string to BigInt first. The benefit is zero risk of precision loss at any point in the serialization chain and no dependency on special JSON libraries. For public APIs serving multiple client languages, string IDs are the safest and most interoperable choice.

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-05.