DEV Community

Ben Lesh
Ben Lesh

Posted on

BigInt and JSON.stringify/JSON.parse

As of this writing, JavaScript's JSON.parse cannot serialize the new JavaScript type BigInt.

Imagine you have the following:

const data = {
  value1: BigInt('1231231231231231213'),
  deep: {
    // NOTE the "n" at the end -- also a BigInt!
    value2: 848484848484848484884n,
  }
}
Enter fullscreen mode Exit fullscreen mode

If you try to just JSON.stringify(data) you will get the error TypeError: Do not know how to serialize a BigInt.

Serialization and Deserialization

It should be noted that how you choose to serialize your BigInts affects how you deserialize your BigInts. Generally, I serialize them by doing appending the "n" suffix to the end, similar to how we can declare a BigInt inline. (BigInt(0) and 0n yield the same result).

Serialization

Here we use JSON.stringify's second argument (It's not always null!!! haha.) which is the replacer. The job of this function, if provided, is to determine how to serialize something based off of it's key and value. If the typeof the value is "bigint", we're going to convert it to a string, and tack an "n" to the end.

// Serialization
const json = JSON.stringify(data, (key, value) =>
  typeof value === "bigint" ? value.toString() + "n" : value
);
Enter fullscreen mode Exit fullscreen mode

The result: json is:

{
  "value1": "1231231231231231213n",
  "deep": {
    "value2": "848484848484848484884n",
  }
}
Enter fullscreen mode Exit fullscreen mode

Deserialization

In order to deserialize what we have above, we can use the second argument to JSON.parse(). (I bet most people didn't know it has a second argument) This is called the reviver, and it's job is to do basically the opposite of the replacer above.

Here we'll test for the type and shape of the value to see that it matches a bunch of numbers followed by an "n".

// Deserialize
const backAgain = JSON.parse(json, (key, value) => {
  if (typeof value === "string" && /^\d+n$/.test(value)) {
    return BigInt(value.substr(0, value.length - 1));
  }
  return value;
});
Enter fullscreen mode Exit fullscreen mode

Alternative serializations

This is all a little tricky, because you have to be sure that none of your other data is in a format where it's a bunch of numbers and an "n" at the end. If it is, you need to change your serialization strategy. For example, perhaps you serialize to BigInt::1231232123 and deserialize the same at the other side, such as the example below:

// Serialize
const json = JSON.stringify(data, (key, value) =>
  typeof value === "bigint" ? `BIGINT::${value}` : value
);

// Deserialize
const backAgain = JSON.parse(json, (key, value) => {
  if (typeof value === "string" && value.startsWith('BIGINT::')) {
    return BigInt(value.substr(8));
  }
  return value;
});
Enter fullscreen mode Exit fullscreen mode

The choice is really up to you, just as long as you have the tools to do it.

Oldest comments (3)

Collapse
 
dustinsoftware profile image
masters.🤔

Very cool, TIL that parse takes a second argument!

In the serialization and deserialization snippets, looks like there's a reference to v which is undefined - looks like it got renamed to value?

Collapse
 
benlesh profile image
Ben Lesh

AH! Typo! thanks. :)

Collapse
 
webreflection profile image
Andrea Giammarchi

Interesting post, thanks, and it'd be great to have a common convention for all native constructors that can't be serialized, and these two helpers should already work for most cases (missing recursive parse/stringify for special cases such as Map or Set).

const serialize = value => `${
  value.constructor.name
}(${
  (typeof value === 'object' && (
    ('length' in value) || ('size' in value))
  ) ?
    `[${[...value].map(v => JSON.stringify(v))}]` :
    value
})`;

const unserialize = value => {
  const i = value.indexOf('(');
  const args = value.slice(i + 1, -1);
  const constructor = self[value.slice(0, i)];
  switch (constructor.name) {
    case 'BigInt':
      return constructor(args);
    default:
      return new constructor(JSON.parse(args));
  }
};
Enter fullscreen mode Exit fullscreen mode

Testable via:

unserialize(serialize(new Set([1, 2, 3])));
unserialize(serialize(new Map([['a', 1], ['b', 2]])));
unserialize(serialize(new Uint8Array([1, 2, 3])));
Enter fullscreen mode Exit fullscreen mode

In CircularJSON, differently from flatted, I used the ~ special char to signal recursion, but having a prefix such as \x01, or any other non common chars sequences, might be all it takes to have more portable data structures that can be resumed.