const assert = require('assert'); const _ = require('lodash'); const ensureJSONEncodable = (obj, path = null, visited = null) => { if (obj === null || _.isBoolean(obj) || _.isNumber(obj) || _.isString(obj)) { return; } path = path || []; if (!_.isPlainObject(obj) && !_.isArray(obj)) { const typeName = typeof obj; const pathStr = path.join('.'); throw new TypeError( `Type '${typeName}' is not JSON-encodable (path: '${pathStr}')` ); } visited = visited || new Set(); if (visited.has(obj)) { const pathStr = path.join('.'); throw new TypeError( `Circular structure is not JSON-encodable (path: '${pathStr}')` ); } visited.add(obj); for (const key in obj) { ensureJSONEncodable(obj[key], path.concat(key), visited); } }; const test = () => { ensureJSONEncodable(null); ensureJSONEncodable(1.23456); ensureJSONEncodable('hello'); ensureJSONEncodable({ foo: 'bar' }); ensureJSONEncodable({ foo: { bar: 'baz' } }); ensureJSONEncodable({ foo: { bar: 'baz', items: [null, 1234, {}] } }); // These object keys are stringified by JSON.stringify(), e.g., 12.34 -> // '12.34', so I guess we can consider them JSON-encodable. ensureJSONEncodable({ 12.34: 56.78 }); ensureJSONEncodable({ [null]: true }); ensureJSONEncodable({ [undefined]: true }); assert.throws( () => { ensureJSONEncodable(undefined); }, { name: 'TypeError', message: "Type 'undefined' is not JSON-encodable (path: '')", } ); assert.throws( () => { ensureJSONEncodable(/hello/); }, { name: 'TypeError', message: "Type 'object' is not JSON-encodable (path: '')", } ); assert.throws( () => { ensureJSONEncodable(console); }, { name: 'TypeError', message: "Type 'object' is not JSON-encodable (path: '')", } ); assert.throws( () => { ensureJSONEncodable({ foo: [null, { xyz: undefined }] }); }, { name: 'TypeError', message: "Type 'undefined' is not JSON-encodable (path: 'foo.1.xyz')", } ); assert.throws( () => { ensureJSONEncodable(function () {}); }, { name: 'TypeError', message: "Type 'function' is not JSON-encodable (path: '')", } ); assert.throws( () => { ensureJSONEncodable({ foo: [null, { hello: function () {} }] }); }, { name: 'TypeError', message: "Type 'function' is not JSON-encodable (path: 'foo.1.hello')", } ); assert.throws( () => { const obj = { num: 1 }; obj.items = [{ self: obj }]; ensureJSONEncodable(obj); }, { name: 'TypeError', message: "Circular structure is not JSON-encodable (path: 'items.0.self')", } ); }; test();