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,
}
}
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
);
The result: json
is:
{
"value1": "1231231231231231213n",
"deep": {
"value2": "848484848484848484884n",
}
}
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;
});
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;
});
The choice is really up to you, just as long as you have the tools to do it.
Top comments (3)
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).
Testable via:
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.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 tovalue
?AH! Typo! thanks. :)