jsbi: JSON.stringify() doesn't know how to serialize a BigInt

Not certain how to handle this situation:

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

      39 |     toObject() {
    > 40 |         return JSON.parse(JSON.stringify(this));
         |                                ^
      41 |     }

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 20
  • Comments: 17

Commits related to this issue

Most upvoted comments

@jakobkummerow Ok, makes sense. For anyone else stumbling upon this, the verified TL;DR fix for the above problem is:

    toObject() {
        return JSON.parse(JSON.stringify(this, (key, value) =>
            typeof value === 'bigint'
                ? value.toString()
                : value // return everything else unchanged
        ));
    }

Wouldn’t it be easier to just monkey patch (hijack) BigInt? That’s the MDN recommendation.

BigInt.prototype.toJSON = function() { return this.toString() }

If you do it this way, you don’t have to add an anonymous function into every call of JSON.stringify. It applies globally to all such calls.

@ ADTC answer will only work with pure javascript. With typescript I get the error: TS2339: Property 'toJSON' does not exist on type 'BigInt'.

@phoenixbeats01 you can do this if you are using typescript

(BigInt.prototype as any).toJSON = function () {
  return this.toString();
};

Or cleaner with the “I know what I do” feature:

BigInt.prototype["toJSON"] = function () {
  return this.toString();
};

Or better (for strict TS configs) with the TS declaration:

// eslint-disable-next-line @typescript-eslint/no-redeclare
interface BigInt {
    /** Convert to BigInt to string form in JSON.stringify */
    toJSON: () => string;
}
BigInt.prototype.toJSON = function () {
    return this.toString();
};

If you’re trying to use this with Typescript, I suggest you create a .ts or .js file with:

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unreachable code error
BigInt.prototype.toJSON = function (): number {
  return this.toString();
};

I found this solution more elegant, works for typescript too:

utils.ts

Object.defineProperty(BigInt.prototype, "toJSON", {
        get() {
            "use strict";
            return () => String(this);
        }
    });

As somebody of you have commented before, you can put this into a file and import it to have it into the global scope with import './utils.js'.

If you’re trying to use this with Typescript, I suggest you create a .ts or .js file with:

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unreachable code error
BigInt.prototype.toJSON = function (): number {
  return this.toString();
};

You can also return Number(this), but I suggest toString as it has no character limiter. However, it will depend on your context.

After that, you must import or require this file in your main file (like index.ts/.js or main.ts, etc).

If you are trying to run tests with Jest, you must add this file in the installation configuration. These settings can be created in package.json with “jest: { … }” or in a file called jest.config.ts (create this in the root).

Example:

export default {
   testTimeout: 30000,
  moduleFileExtensions: ['js', 'json', 'ts'],
  **setupFiles: ['./my_config_file'],**
  testRegex: '.*\\.spec\\.ts$',
  transform: {
    '^.+\\.(t|j)s$': 'ts-jest',
  },
  collectCoverageFrom: ['**/*.(t|j)s'],
  coverageDirectory: '../coverage',
  testEnvironment: 'node',
}

At this point you can create a test module, with many configs, and create an index file that imports all the configs, and just pass this index file in the setupFiles config from jest.

Working as intended/spec’ed: https://tc39.es/proposal-bigint/#sec-serializejsonproperty (step 10).

In short, the background is that JSON is so commonly used among all sorts of different systems and programming languages that it is effectively impossible to add anything to the format without breaking compatibility in some crucial case.

You can define your own .toJSON() function, and pass a corresponding “reviver” function as an argument to JSON.parse(). That way, at least you have control over any (backwards) compatibility issues.

Or cleaner with the “I know what I do” feature:

BigInt.prototype["toJSON"] = function () {
  return this.toString();
};

Element implicitly has an ‘any’ type because expression of type ‘“toJSON”’ can’t be used to index type ‘BigInt’. Property ‘toJSON’ does not exist on type ‘BigInt’.ts(7053)

I guess the process can be like this: stringify:

function replacer(key, value) {
  if (typeof value === 'bigint') {
    return {
      type: 'bigint',
      value: value.toString()
    };
  } else {
    return value;
  }
}

const obj = {
  smallInt: 1,
  n: BigInt(9007199254740991),
};

let s = JSON.stringify(obj, replacer) //{"smallInt":1,"n":{"type":"bigint","value":"9007199254740991"}}

parse:

function reviver(key, value) {
  if (value && value.type == 'bigint') {
    return BigInt(value.value);  
  }
  return value;
}

const parsedObj = JSON.parse(s, reviver); //{ smallInt: 1, n: 9007199254740991n }

Hope this makes sense

I guess the process can be like this: stringify:

function replacer(key, value) {
  if (typeof value === 'bigint') {
    return {
      type: 'bigint',
      value: value.toString()
    };
  } else {
    return value;
  }
}

const obj = {
  smallInt: 1,
  n: BigInt(9007199254740991),
};

let s = JSON.stringify(obj, replacer) //{"smallInt":1,"n":{"type":"bigint","value":"9007199254740991"}}

parse:

function reviver(key, value) {
  if (value && value.type == 'bigint') {
    return BigInt(value.value);  
  }
  return value;
}

const parsedObj = JSON.parse(s, reviver); //{ smallInt: 1, n: 9007199254740991n }

Hope this makes sense

This is it! Here i added a wrapper function:

// Wrapper around JSON stringify/parse methods to support bigint serialization

function replacer(key, value) {
  if (typeof value === "bigint") {
    return {
      __type: "bigint",
      __value: value.toString(),
    };
  } else {
    return value;
  }
}

function reviver(key, value) {
  if (value && value.__type == "bigint") {
    return BigInt(value.__value);
  }
  return value;
}

export const json_stringify = (obj) => {
  return JSON.stringify(obj, replacer);
};

export const json_parse = (s) => {
  return JSON.parse(s, reviver);
};

JSON.parse(JSON.stringify(String(this))) worked for me.

The issue with serializing a BigInt as a string is that during the parsing you must know what any property is to know which ones should stay as strings and which ones should become big integers.

It would be better to use an approach similar to how BigNumber instances are stringified like this

{ 
  "type": "BigNumber", 
  "hex": "0x01e848"
}

I just added to bigNumberify support for BigInt values, so that the BigInt 125000n will be encoded as

{ 
  "type": "BigInt", 
  "hex": "0x01e848"
}

and parsed back without ambiguities. Any feedback would be appreciated.

Polyfill:

// Polyfill: BigInt.prototype.toJSON 

(function(global, undefined) {
  if (global.BigInt.prototype.toJSON === undefined) {
    global.Object.defineProperty(global.BigInt.prototype, "toJSON", {
        value: function() { return this.toString(); },
        configurable: true,
        enumerable: false,
        writable: true
    });
  }
})(window !== void 0 ? window : typeof global !== void 0 ? global : typeof self !== void 0 ? self : this);

Element implicitly has an ‘any’ type because expression of type ‘“toJSON”’ can’t be used to index type ‘BigInt’. Property ‘toJSON’ does not exist on type ‘BigInt’.ts(7053)

Oh, I had suppressImplicitAnyIndexErrors: true in my config. Edited my example. Thanks!