├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── benchmark.js ├── compare.js ├── esm ├── package.json ├── wrapper.d.ts └── wrapper.js ├── index.d.ts ├── index.js ├── package.json ├── readme.md ├── test.js ├── test.json └── tsconfig.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | node_version: 18 | - 10 19 | - 12 20 | - 14 21 | - 16 22 | - 18 23 | - 20 24 | - 22 25 | name: Running tests with Node ${{ matrix.node_version }} on ${{ matrix.os }} 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v2 30 | with: 31 | node-version: ${{ matrix.node_version }} 32 | - run: | 33 | npm install 34 | npm run test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | yarn.lock 5 | coverage 6 | .nyc_output 7 | .vscode 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !index.d.ts 3 | !index.js 4 | !LICENSE 5 | !package.json 6 | !esm/* 7 | !readme.md 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v2.6.0 4 | 5 | - Added `safe` option to not fail in case a getter or `.toJSON()` throws an error. 6 | Instead, a string as error message is replacing the object inspection. This allows to partially inspect such objects. 7 | The default is `false` to prevent any breaking change. 8 | 9 | ```js 10 | import { configure } from 'safe-stable-stringify' 11 | 12 | const stringify = configure({ 13 | safe: true 14 | }) 15 | 16 | stringify([{ 17 | foo: { a: 5, get foo() { throw new Error('Oops') }, c: true } 18 | }]) 19 | // '[{"foo":"Error: Stringification failed. Message: Oops"}]' 20 | 21 | stringify([{ 22 | foo: { a: 5, toJSON() { throw new Error('Oops') }, c: true } 23 | }]) 24 | // '[{"foo":"Error: Stringification failed. Message: Oops"}]' 25 | ``` 26 | 27 | ## v2.5.0 28 | 29 | - Accept `Array#sort(comparator)` comparator method as deterministic option value to use that comparator for sorting object keys. 30 | 31 | ```js 32 | import { configure } from 'safe-stable-stringify' 33 | 34 | const object = { 35 | a: 1, 36 | b: 2, 37 | c: 3, 38 | } 39 | 40 | const stringify = configure({ 41 | deterministic: (a, b) => b.localeCompare(a) 42 | }) 43 | 44 | stringify(object) 45 | // '{"c":3,"b":2,"a":1}' 46 | ``` 47 | 48 | - Very minor performance optimization. 49 | 50 | Thanks to @flobernd, @cesco69 and @prisis to contribute to this release! 51 | 52 | ## v2.4.3 53 | 54 | - Fixed toJSON function receiving array keys as number instead of string 55 | - Fixed replacer function receiving array keys as number instead of string 56 | - Fixed replacer function not being called for TypedArray entries 57 | - Improved performance to escape long strings that contain characters that need escaping 58 | 59 | ## v2.4.2 60 | 61 | - Improved ESM TypeScript types. 62 | - More precise TypeScript replacer type. 63 | 64 | ## v2.4.1 65 | 66 | - More precise TypeScript types. The return type is now either `string`, `undefined` or `string | undefined` depending on the input. 67 | 68 | ## v2.4.0 69 | 70 | - Added `strict` option to verify that the passed in objects are fully compatible with JSON without removing information. If not, an error is thrown. 71 | - Fixed TypeScript definition for ESM code bases 72 | 73 | ## v2.3.1 74 | 75 | - Fix `invalid regexp group` error in browsers or environments that do not support the negative lookbehind regular expression assertion. 76 | 77 | ## v2.3.0 78 | 79 | - Accept the `Error` constructor as `circularValue` option to throw on circular references as the regular JSON.stringify would: 80 | 81 | ```js 82 | import { configure } from 'safe-stable-stringify' 83 | 84 | const object = {} 85 | object.circular = object; 86 | 87 | const stringify = configure({ circularValue: TypeError }) 88 | 89 | stringify(object) 90 | // TypeError: Converting circular structure to JSON 91 | ``` 92 | 93 | - Fixed escaping wrong surrogates. Only lone surrogates are now escaped. 94 | 95 | ## v2.2.0 96 | 97 | - Reduce module size by removing the test and benchmark files from the published package 98 | - Accept `undefined` as `circularValue` option to remove circular properties from the serialized output: 99 | 100 | ```js 101 | import { configure } from 'safe-stable-stringify' 102 | 103 | const object = { array: [] } 104 | object.circular = object; 105 | object.array.push(object) 106 | 107 | configure({ circularValue: undefined })(object) 108 | // '{"array":[null]}' 109 | ``` 110 | 111 | ## v2.1.0 112 | 113 | - Added `maximumBreadth` option to limit stringification at a specific object or array "width" (number of properties / values) 114 | - Added `maximumDepth` option to limit stringification at a specific nesting depth 115 | - Implemented the [well formed stringify proposal](https://github.com/tc39/proposal-well-formed-stringify) that is now part of the spec 116 | - Fixed maximum spacer length (10) 117 | - Fixed TypeScript definition 118 | - Fixed duplicated array replacer values serialized more than once 119 | 120 | ## v2.0.0 121 | 122 | - __[BREAKING]__ Convert BigInt to number by default instead of ignoring these values 123 | If you wish to ignore these values similar to earlier versions, just use the new `bigint` option and set it to `false`. 124 | - __[BREAKING]__ Support ESM 125 | - __[BREAKING]__ Requires ES6 126 | - Optional BigInt support 127 | - Deterministic behavior is now optional 128 | - The value to indicate a circular structure is now adjustable 129 | - Significantly faster TypedArray stringification 130 | - Smaller Codebase 131 | - Removed stateful indentation to guarantee side-effect freeness 132 | 133 | ## v1.1.1 134 | 135 | - Fixed an indentation issue in combination with empty arrays and objects 136 | - Updated dev dependencies 137 | 138 | ## v1.1.0 139 | 140 | - Add support for IE11 (https://github.com/BridgeAR/safe-stable-stringify/commit/917b6128de135a950ec178d66d86b4d772c7656d) 141 | - Fix issue with undefined values (https://github.com/BridgeAR/safe-stable-stringify/commit/4196f87, https://github.com/BridgeAR/safe-stable-stringify/commit/4eab558) 142 | - Fix typescript definition (https://github.com/BridgeAR/safe-stable-stringify/commit/7a87478) 143 | - Improve code coverage (https://github.com/BridgeAR/safe-stable-stringify/commit/ed8cadc, https://github.com/BridgeAR/safe-stable-stringify/commit/b58c494) 144 | - Update dev dependencies (https://github.com/BridgeAR/safe-stable-stringify/commit/b857ea8) 145 | - Improve docs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Ruben Bridgewater 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Benchmark = require('bench-node') 4 | const suite = new Benchmark.Suite() 5 | const stringify = require('.').configure({ deterministic: true }) 6 | 7 | const array = Array.from({ length: 10 }, (_, i) => i) 8 | const obj = { array } 9 | const circ = JSON.parse(JSON.stringify(obj)) 10 | circ.o = { obj: circ, array } 11 | 12 | const deep = require('./package.json') 13 | deep.deep = JSON.parse(JSON.stringify(deep)) 14 | deep.deep.deep = JSON.parse(JSON.stringify(deep)) 15 | deep.deep.deep.deep = JSON.parse(JSON.stringify(deep)) 16 | deep.array = array 17 | 18 | const deepCirc = JSON.parse(JSON.stringify(deep)) 19 | deepCirc.deep.deep.deep.circ = deepCirc 20 | deepCirc.deep.deep.circ = deepCirc 21 | deepCirc.deep.circ = deepCirc 22 | deepCirc.array = array 23 | 24 | // One arg "simple" 25 | suite.add('simple: simple object', function () { 26 | stringify(obj) 27 | }) 28 | suite.add('simple: circular ', function () { 29 | stringify(circ) 30 | }) 31 | suite.add('simple: deep ', function () { 32 | stringify(deep) 33 | }) 34 | suite.add('simple: deep circular', function () { 35 | stringify(deepCirc) 36 | }) 37 | 38 | // Two args "replacer" 39 | suite.add('\nreplacer: simple object', function () { 40 | stringify(obj, (_, v) => v) 41 | }) 42 | suite.add('replacer: circular ', function () { 43 | stringify(circ, (_, v) => v) 44 | }) 45 | suite.add('replacer: deep ', function () { 46 | stringify(deep, (_, v) => v) 47 | }) 48 | suite.add('replacer: deep circular', function () { 49 | stringify(deepCirc, (_, v) => v) 50 | }) 51 | 52 | // Two args "array" 53 | suite.add('\narray: simple object', function () { 54 | stringify(obj, ['array']) 55 | }) 56 | suite.add('array: circular ', function () { 57 | stringify(circ, ['array']) 58 | }) 59 | suite.add('array: deep ', function () { 60 | stringify(deep, ['array']) 61 | }) 62 | suite.add('array: deep circular', function () { 63 | stringify(deepCirc, ['array']) 64 | }) 65 | 66 | // Three args "full replacer" 67 | suite.add('\nfull replacer: simple object', function () { 68 | stringify(obj, (_, v) => v, 2) 69 | }) 70 | suite.add('full replacer: circular ', function () { 71 | stringify(circ, (_, v) => v, 2) 72 | }) 73 | suite.add('full replacer: deep ', function () { 74 | stringify(deep, (_, v) => v, 2) 75 | }) 76 | suite.add('full replacer: deep circular', function () { 77 | stringify(deepCirc, (_, v) => v, 2) 78 | }) 79 | 80 | // Three args "full array" 81 | suite.add('\nfull array: simple object', function () { 82 | stringify(obj, ['array'], 2) 83 | }) 84 | suite.add('full array: circular ', function () { 85 | stringify(circ, ['array'], 2) 86 | }) 87 | suite.add('full array: deep ', function () { 88 | stringify(deep, ['array'], 2) 89 | }) 90 | suite.add('full array: deep circular', function () { 91 | stringify(deepCirc, ['array'], 2) 92 | }) 93 | 94 | // Three args "indentation only" 95 | suite.add('\nindentation: simple object', function () { 96 | stringify(obj, null, 2) 97 | }) 98 | suite.add('indentation: circular ', function () { 99 | stringify(circ, null, 2) 100 | }) 101 | suite.add('indentation: deep ', function () { 102 | stringify(deep, null, 2) 103 | }) 104 | suite.add('indentation: deep circular', function () { 105 | stringify(deepCirc, null, 2) 106 | }) 107 | 108 | suite.run() 109 | -------------------------------------------------------------------------------- /compare.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Benchmark = require('bench-node') 4 | const suite = new Benchmark.Suite() 5 | const testData = require('./test.json') 6 | 7 | const stringifyPackages = { 8 | // 'JSON.stringify': JSON.stringify, 9 | 'fastest-stable-stringify': true, 10 | 'fast-json-stable-stringify': true, 11 | 'json-stable-stringify': true, 12 | 'fast-stable-stringify': true, 13 | 'faster-stable-stringify': true, 14 | 'json-stringify-deterministic': true, 15 | 'fast-safe-stringify': 'stable', 16 | 'safe-stable-stringify': require('.') 17 | } 18 | 19 | for (const name in stringifyPackages) { 20 | let fn 21 | if (typeof stringifyPackages[name] === 'function') { 22 | fn = stringifyPackages[name] 23 | } else if (typeof stringifyPackages[name] === 'string') { 24 | fn = require(name)[stringifyPackages[name]] 25 | } else { 26 | fn = require(name) 27 | } 28 | 29 | suite.add(name, function () { 30 | fn(testData) 31 | }) 32 | } 33 | 34 | suite.run() 35 | -------------------------------------------------------------------------------- /esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "main": "wrapper.js" 4 | } 5 | -------------------------------------------------------------------------------- /esm/wrapper.d.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from '../index.js' 2 | 3 | export * from '../index.js' 4 | export default stringify 5 | -------------------------------------------------------------------------------- /esm/wrapper.js: -------------------------------------------------------------------------------- 1 | import cjsModule from '../index.js' 2 | 3 | export const configure = cjsModule.configure 4 | 5 | export const stringify = cjsModule 6 | export default cjsModule 7 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type Replacer = (number | string)[] | null | undefined | ((key: string, value: unknown) => string | number | boolean | null | object) 2 | 3 | export function stringify(value: undefined | symbol | ((...args: unknown[]) => unknown), replacer?: Replacer, space?: string | number): undefined 4 | export function stringify(value: string | number | unknown[] | null | boolean | object, replacer?: Replacer, space?: string | number): string 5 | export function stringify(value: unknown, replacer?: ((key: string, value: unknown) => unknown) | (number | string)[] | null | undefined, space?: string | number): string | undefined 6 | 7 | export interface StringifyOptions { 8 | bigint?: boolean, 9 | circularValue?: string | null | TypeErrorConstructor | ErrorConstructor, 10 | deterministic?: boolean | ((a: string, b: string) => number), 11 | maximumBreadth?: number, 12 | maximumDepth?: number, 13 | strict?: boolean, 14 | safe?: boolean, 15 | } 16 | 17 | export namespace stringify { 18 | export function configure(options: StringifyOptions): typeof stringify 19 | } 20 | 21 | export function configure(options: StringifyOptions): typeof stringify 22 | 23 | export default stringify 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { hasOwnProperty } = Object.prototype 4 | 5 | const stringify = configure() 6 | 7 | // @ts-expect-error 8 | stringify.configure = configure 9 | // @ts-expect-error 10 | stringify.stringify = stringify 11 | 12 | // @ts-expect-error 13 | stringify.default = stringify 14 | 15 | // @ts-expect-error used for named export 16 | exports.stringify = stringify 17 | // @ts-expect-error used for named export 18 | exports.configure = configure 19 | 20 | module.exports = stringify 21 | 22 | // eslint-disable-next-line no-control-regex 23 | const strEscapeSequencesRegExp = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/ 24 | 25 | // Escape C0 control characters, double quotes, the backslash and every code 26 | // unit with a numeric value in the inclusive range 0xD800 to 0xDFFF. 27 | function strEscape (str) { 28 | // Some magic numbers that worked out fine while benchmarking with v8 8.0 29 | if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) { 30 | return `"${str}"` 31 | } 32 | return JSON.stringify(str) 33 | } 34 | 35 | function sort (array, comparator) { 36 | // Insertion sort is very efficient for small input sizes, but it has a bad 37 | // worst case complexity. Thus, use native array sort for bigger values. 38 | if (array.length > 2e2 || comparator) { 39 | return array.sort(comparator) 40 | } 41 | for (let i = 1; i < array.length; i++) { 42 | const currentValue = array[i] 43 | let position = i 44 | while (position !== 0 && array[position - 1] > currentValue) { 45 | array[position] = array[position - 1] 46 | position-- 47 | } 48 | array[position] = currentValue 49 | } 50 | return array 51 | } 52 | 53 | const typedArrayPrototypeGetSymbolToStringTag = 54 | Object.getOwnPropertyDescriptor( 55 | Object.getPrototypeOf( 56 | Object.getPrototypeOf( 57 | new Int8Array() 58 | ) 59 | ), 60 | Symbol.toStringTag 61 | ).get 62 | 63 | function isTypedArrayWithEntries (value) { 64 | return typedArrayPrototypeGetSymbolToStringTag.call(value) !== undefined && value.length !== 0 65 | } 66 | 67 | function stringifyTypedArray (array, separator, maximumBreadth) { 68 | if (array.length < maximumBreadth) { 69 | maximumBreadth = array.length 70 | } 71 | const whitespace = separator === ',' ? '' : ' ' 72 | let res = `"0":${whitespace}${array[0]}` 73 | for (let i = 1; i < maximumBreadth; i++) { 74 | res += `${separator}"${i}":${whitespace}${array[i]}` 75 | } 76 | return res 77 | } 78 | 79 | function getCircularValueOption (options) { 80 | if (hasOwnProperty.call(options, 'circularValue')) { 81 | const circularValue = options.circularValue 82 | if (typeof circularValue === 'string') { 83 | return `"${circularValue}"` 84 | } 85 | if (circularValue == null) { 86 | return circularValue 87 | } 88 | if (circularValue === Error || circularValue === TypeError) { 89 | return { 90 | toString () { 91 | throw new TypeError('Converting circular structure to JSON') 92 | } 93 | } 94 | } 95 | throw new TypeError('The "circularValue" argument must be of type string or the value null or undefined') 96 | } 97 | return '"[Circular]"' 98 | } 99 | 100 | function getDeterministicOption (options) { 101 | let value 102 | if (hasOwnProperty.call(options, 'deterministic')) { 103 | value = options.deterministic 104 | if (typeof value !== 'boolean' && typeof value !== 'function') { 105 | throw new TypeError('The "deterministic" argument must be of type boolean or comparator function') 106 | } 107 | } 108 | return value === undefined ? true : value 109 | } 110 | 111 | function getBooleanOption (options, key, defaultValue = true) { 112 | let value 113 | if (hasOwnProperty.call(options, key)) { 114 | value = options[key] 115 | if (typeof value !== 'boolean') { 116 | throw new TypeError(`The "${key}" argument must be of type boolean`) 117 | } 118 | } 119 | return value === undefined ? defaultValue : value 120 | } 121 | 122 | function getPositiveIntegerOption (options, key) { 123 | let value 124 | if (hasOwnProperty.call(options, key)) { 125 | value = options[key] 126 | if (typeof value !== 'number') { 127 | throw new TypeError(`The "${key}" argument must be of type number`) 128 | } 129 | if (!Number.isInteger(value)) { 130 | throw new TypeError(`The "${key}" argument must be an integer`) 131 | } 132 | if (value < 1) { 133 | throw new RangeError(`The "${key}" argument must be >= 1`) 134 | } 135 | } 136 | return value === undefined ? Infinity : value 137 | } 138 | 139 | function getItemCount (number) { 140 | if (number === 1) { 141 | return '1 item' 142 | } 143 | return `${number} items` 144 | } 145 | 146 | function getUniqueReplacerSet (replacerArray) { 147 | const replacerSet = new Set() 148 | for (const value of replacerArray) { 149 | if (typeof value === 'string' || typeof value === 'number') { 150 | replacerSet.add(String(value)) 151 | } 152 | } 153 | return replacerSet 154 | } 155 | 156 | function getStrictOption (options) { 157 | if (hasOwnProperty.call(options, 'strict')) { 158 | const value = options.strict 159 | if (typeof value !== 'boolean') { 160 | throw new TypeError('The "strict" argument must be of type boolean') 161 | } 162 | if (value) { 163 | return (value) => { 164 | let message = `Object can not safely be stringified. Received type ${typeof value}` 165 | if (typeof value !== 'function') message += ` (${value.toString()})` 166 | throw new Error(message) 167 | } 168 | } 169 | } 170 | } 171 | 172 | function makeSafe (method) { 173 | return function (...input) { 174 | try { 175 | return method(...input) 176 | } catch (error) { 177 | const message = typeof error?.message === 'string' 178 | ? error.message 179 | : (() => { try { return String(error) } catch { return 'Failed' } })() 180 | return strEscape('Error: Stringification failed. Message: ' + message) 181 | } 182 | } 183 | } 184 | 185 | function configure (options) { 186 | options = { ...options } 187 | const fail = getStrictOption(options) 188 | if (fail) { 189 | if (options.bigint === undefined) { 190 | options.bigint = false 191 | } 192 | if (!('circularValue' in options)) { 193 | options.circularValue = Error 194 | } 195 | } 196 | const circularValue = getCircularValueOption(options) 197 | const bigint = getBooleanOption(options, 'bigint') 198 | const deterministic = getDeterministicOption(options) 199 | const comparator = typeof deterministic === 'function' ? deterministic : undefined 200 | const maximumDepth = getPositiveIntegerOption(options, 'maximumDepth') 201 | const maximumBreadth = getPositiveIntegerOption(options, 'maximumBreadth') 202 | const safe = getBooleanOption(options, 'safe', false) 203 | 204 | let stringifyFnReplacer = function (key, parent, stack, replacer, spacer, indentation) { 205 | let value = parent[key] 206 | 207 | if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') { 208 | value = value.toJSON(key) 209 | } 210 | value = replacer.call(parent, key, value) 211 | 212 | switch (typeof value) { 213 | case 'string': 214 | return strEscape(value) 215 | case 'object': { 216 | if (value === null) { 217 | return 'null' 218 | } 219 | if (stack.indexOf(value) !== -1) { 220 | return circularValue 221 | } 222 | 223 | let res = '' 224 | let join = ',' 225 | const originalIndentation = indentation 226 | 227 | if (Array.isArray(value)) { 228 | if (value.length === 0) { 229 | return '[]' 230 | } 231 | if (maximumDepth < stack.length + 1) { 232 | return '"[Array]"' 233 | } 234 | stack.push(value) 235 | if (spacer !== '') { 236 | indentation += spacer 237 | res += `\n${indentation}` 238 | join = `,\n${indentation}` 239 | } 240 | const maximumValuesToStringify = Math.min(value.length, maximumBreadth) 241 | let i = 0 242 | for (; i < maximumValuesToStringify - 1; i++) { 243 | const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation) 244 | res += tmp !== undefined ? tmp : 'null' 245 | res += join 246 | } 247 | const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation) 248 | res += tmp !== undefined ? tmp : 'null' 249 | if (value.length - 1 > maximumBreadth) { 250 | const removedKeys = value.length - maximumBreadth - 1 251 | res += `${join}"... ${getItemCount(removedKeys)} not stringified"` 252 | } 253 | if (spacer !== '') { 254 | res += `\n${originalIndentation}` 255 | } 256 | stack.pop() 257 | return `[${res}]` 258 | } 259 | 260 | let keys = Object.keys(value) 261 | const keyLength = keys.length 262 | if (keyLength === 0) { 263 | return '{}' 264 | } 265 | if (maximumDepth < stack.length + 1) { 266 | return '"[Object]"' 267 | } 268 | let whitespace = '' 269 | let separator = '' 270 | if (spacer !== '') { 271 | indentation += spacer 272 | join = `,\n${indentation}` 273 | whitespace = ' ' 274 | } 275 | const maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth) 276 | if (deterministic && !isTypedArrayWithEntries(value)) { 277 | keys = sort(keys, comparator) 278 | } 279 | stack.push(value) 280 | for (let i = 0; i < maximumPropertiesToStringify; i++) { 281 | const key = keys[i] 282 | const tmp = stringifyFnReplacer(key, value, stack, replacer, spacer, indentation) 283 | if (tmp !== undefined) { 284 | res += `${separator}${strEscape(key)}:${whitespace}${tmp}` 285 | separator = join 286 | } 287 | } 288 | if (keyLength > maximumBreadth) { 289 | const removedKeys = keyLength - maximumBreadth 290 | res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"` 291 | separator = join 292 | } 293 | if (spacer !== '' && separator.length > 1) { 294 | res = `\n${indentation}${res}\n${originalIndentation}` 295 | } 296 | stack.pop() 297 | return `{${res}}` 298 | } 299 | case 'number': 300 | return isFinite(value) ? String(value) : fail ? fail(value) : 'null' 301 | case 'boolean': 302 | return value === true ? 'true' : 'false' 303 | case 'undefined': 304 | return undefined 305 | case 'bigint': 306 | if (bigint) { 307 | return String(value) 308 | } 309 | // fallthrough 310 | default: 311 | return fail ? fail(value) : undefined 312 | } 313 | } 314 | 315 | let stringifyArrayReplacer = function (key, value, stack, replacer, spacer, indentation) { 316 | if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') { 317 | value = value.toJSON(key) 318 | } 319 | 320 | switch (typeof value) { 321 | case 'string': 322 | return strEscape(value) 323 | case 'object': { 324 | if (value === null) { 325 | return 'null' 326 | } 327 | if (stack.indexOf(value) !== -1) { 328 | return circularValue 329 | } 330 | 331 | const originalIndentation = indentation 332 | let res = '' 333 | let join = ',' 334 | 335 | if (Array.isArray(value)) { 336 | if (value.length === 0) { 337 | return '[]' 338 | } 339 | if (maximumDepth < stack.length + 1) { 340 | return '"[Array]"' 341 | } 342 | stack.push(value) 343 | if (spacer !== '') { 344 | indentation += spacer 345 | res += `\n${indentation}` 346 | join = `,\n${indentation}` 347 | } 348 | const maximumValuesToStringify = Math.min(value.length, maximumBreadth) 349 | let i = 0 350 | for (; i < maximumValuesToStringify - 1; i++) { 351 | const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation) 352 | res += tmp !== undefined ? tmp : 'null' 353 | res += join 354 | } 355 | const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation) 356 | res += tmp !== undefined ? tmp : 'null' 357 | if (value.length - 1 > maximumBreadth) { 358 | const removedKeys = value.length - maximumBreadth - 1 359 | res += `${join}"... ${getItemCount(removedKeys)} not stringified"` 360 | } 361 | if (spacer !== '') { 362 | res += `\n${originalIndentation}` 363 | } 364 | stack.pop() 365 | return `[${res}]` 366 | } 367 | stack.push(value) 368 | let whitespace = '' 369 | if (spacer !== '') { 370 | indentation += spacer 371 | join = `,\n${indentation}` 372 | whitespace = ' ' 373 | } 374 | let separator = '' 375 | for (const key of replacer) { 376 | const tmp = stringifyArrayReplacer(key, value[key], stack, replacer, spacer, indentation) 377 | if (tmp !== undefined) { 378 | res += `${separator}${strEscape(key)}:${whitespace}${tmp}` 379 | separator = join 380 | } 381 | } 382 | if (spacer !== '' && separator.length > 1) { 383 | res = `\n${indentation}${res}\n${originalIndentation}` 384 | } 385 | stack.pop() 386 | return `{${res}}` 387 | } 388 | case 'number': 389 | return isFinite(value) ? String(value) : fail ? fail(value) : 'null' 390 | case 'boolean': 391 | return value === true ? 'true' : 'false' 392 | case 'undefined': 393 | return undefined 394 | case 'bigint': 395 | if (bigint) { 396 | return String(value) 397 | } 398 | // fallthrough 399 | default: 400 | return fail ? fail(value) : undefined 401 | } 402 | } 403 | 404 | let stringifyIndent = function (key, value, stack, spacer, indentation) { 405 | switch (typeof value) { 406 | case 'string': 407 | return strEscape(value) 408 | case 'object': { 409 | if (value === null) { 410 | return 'null' 411 | } 412 | if (typeof value.toJSON === 'function') { 413 | value = value.toJSON(key) 414 | // Prevent calling `toJSON` again. 415 | if (typeof value !== 'object') { 416 | return stringifyIndent(key, value, stack, spacer, indentation) 417 | } 418 | if (value === null) { 419 | return 'null' 420 | } 421 | } 422 | if (stack.indexOf(value) !== -1) { 423 | return circularValue 424 | } 425 | const originalIndentation = indentation 426 | 427 | if (Array.isArray(value)) { 428 | if (value.length === 0) { 429 | return '[]' 430 | } 431 | if (maximumDepth < stack.length + 1) { 432 | return '"[Array]"' 433 | } 434 | stack.push(value) 435 | indentation += spacer 436 | let res = `\n${indentation}` 437 | const join = `,\n${indentation}` 438 | const maximumValuesToStringify = Math.min(value.length, maximumBreadth) 439 | let i = 0 440 | for (; i < maximumValuesToStringify - 1; i++) { 441 | const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation) 442 | res += tmp !== undefined ? tmp : 'null' 443 | res += join 444 | } 445 | const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation) 446 | res += tmp !== undefined ? tmp : 'null' 447 | if (value.length - 1 > maximumBreadth) { 448 | const removedKeys = value.length - maximumBreadth - 1 449 | res += `${join}"... ${getItemCount(removedKeys)} not stringified"` 450 | } 451 | res += `\n${originalIndentation}` 452 | stack.pop() 453 | return `[${res}]` 454 | } 455 | 456 | let keys = Object.keys(value) 457 | const keyLength = keys.length 458 | if (keyLength === 0) { 459 | return '{}' 460 | } 461 | if (maximumDepth < stack.length + 1) { 462 | return '"[Object]"' 463 | } 464 | indentation += spacer 465 | const join = `,\n${indentation}` 466 | let res = '' 467 | let separator = '' 468 | let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth) 469 | if (isTypedArrayWithEntries(value)) { 470 | res += stringifyTypedArray(value, join, maximumBreadth) 471 | keys = keys.slice(value.length) 472 | maximumPropertiesToStringify -= value.length 473 | separator = join 474 | } 475 | if (deterministic) { 476 | keys = sort(keys, comparator) 477 | } 478 | stack.push(value) 479 | for (let i = 0; i < maximumPropertiesToStringify; i++) { 480 | const key = keys[i] 481 | const tmp = stringifyIndent(key, value[key], stack, spacer, indentation) 482 | if (tmp !== undefined) { 483 | res += `${separator}${strEscape(key)}: ${tmp}` 484 | separator = join 485 | } 486 | } 487 | if (keyLength > maximumBreadth) { 488 | const removedKeys = keyLength - maximumBreadth 489 | res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"` 490 | separator = join 491 | } 492 | if (separator !== '') { 493 | res = `\n${indentation}${res}\n${originalIndentation}` 494 | } 495 | stack.pop() 496 | return `{${res}}` 497 | } 498 | case 'number': 499 | return isFinite(value) ? String(value) : fail ? fail(value) : 'null' 500 | case 'boolean': 501 | return value === true ? 'true' : 'false' 502 | case 'undefined': 503 | return undefined 504 | case 'bigint': 505 | if (bigint) { 506 | return String(value) 507 | } 508 | // fallthrough 509 | default: 510 | return fail ? fail(value) : undefined 511 | } 512 | } 513 | 514 | let stringifySimple = function (key, value, stack) { 515 | switch (typeof value) { 516 | case 'string': 517 | return strEscape(value) 518 | case 'object': { 519 | if (value === null) { 520 | return 'null' 521 | } 522 | if (typeof value.toJSON === 'function') { 523 | value = value.toJSON(key) 524 | // Prevent calling `toJSON` again 525 | if (typeof value !== 'object') { 526 | return stringifySimple(key, value, stack) 527 | } 528 | if (value === null) { 529 | return 'null' 530 | } 531 | } 532 | if (stack.indexOf(value) !== -1) { 533 | return circularValue 534 | } 535 | 536 | let res = '' 537 | 538 | const hasLength = value.length !== undefined 539 | if (hasLength && Array.isArray(value)) { 540 | if (value.length === 0) { 541 | return '[]' 542 | } 543 | if (maximumDepth < stack.length + 1) { 544 | return '"[Array]"' 545 | } 546 | stack.push(value) 547 | const maximumValuesToStringify = Math.min(value.length, maximumBreadth) 548 | let i = 0 549 | for (; i < maximumValuesToStringify - 1; i++) { 550 | const tmp = stringifySimple(String(i), value[i], stack) 551 | res += tmp !== undefined ? tmp : 'null' 552 | res += ',' 553 | } 554 | const tmp = stringifySimple(String(i), value[i], stack) 555 | res += tmp !== undefined ? tmp : 'null' 556 | if (value.length - 1 > maximumBreadth) { 557 | const removedKeys = value.length - maximumBreadth - 1 558 | res += `,"... ${getItemCount(removedKeys)} not stringified"` 559 | } 560 | stack.pop() 561 | return `[${res}]` 562 | } 563 | 564 | let keys = Object.keys(value) 565 | const keyLength = keys.length 566 | if (keyLength === 0) { 567 | return '{}' 568 | } 569 | if (maximumDepth < stack.length + 1) { 570 | return '"[Object]"' 571 | } 572 | let separator = '' 573 | let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth) 574 | if (hasLength && isTypedArrayWithEntries(value)) { 575 | res += stringifyTypedArray(value, ',', maximumBreadth) 576 | keys = keys.slice(value.length) 577 | maximumPropertiesToStringify -= value.length 578 | separator = ',' 579 | } 580 | if (deterministic) { 581 | keys = sort(keys, comparator) 582 | } 583 | stack.push(value) 584 | for (let i = 0; i < maximumPropertiesToStringify; i++) { 585 | const key = keys[i] 586 | const tmp = stringifySimple(key, value[key], stack) 587 | if (tmp !== undefined) { 588 | res += `${separator}${strEscape(key)}:${tmp}` 589 | separator = ',' 590 | } 591 | } 592 | if (keyLength > maximumBreadth) { 593 | const removedKeys = keyLength - maximumBreadth 594 | res += `${separator}"...":"${getItemCount(removedKeys)} not stringified"` 595 | } 596 | stack.pop() 597 | return `{${res}}` 598 | } 599 | case 'number': 600 | return isFinite(value) ? String(value) : fail ? fail(value) : 'null' 601 | case 'boolean': 602 | return value === true ? 'true' : 'false' 603 | case 'undefined': 604 | return undefined 605 | case 'bigint': 606 | if (bigint) { 607 | return String(value) 608 | } 609 | // fallthrough 610 | default: 611 | return fail ? fail(value) : undefined 612 | } 613 | } 614 | 615 | if (safe) { 616 | stringifyFnReplacer = makeSafe(stringifyFnReplacer) 617 | stringifyArrayReplacer = makeSafe(stringifyArrayReplacer) 618 | stringifyIndent = makeSafe(stringifyIndent) 619 | stringifySimple = makeSafe(stringifySimple) 620 | } 621 | 622 | function stringify (value, replacer, space) { 623 | if (arguments.length > 1) { 624 | let spacer = '' 625 | if (typeof space === 'number') { 626 | spacer = ' '.repeat(Math.min(space, 10)) 627 | } else if (typeof space === 'string') { 628 | spacer = space.slice(0, 10) 629 | } 630 | if (replacer != null) { 631 | if (typeof replacer === 'function') { 632 | return stringifyFnReplacer('', { '': value }, [], replacer, spacer, '') 633 | } 634 | if (Array.isArray(replacer)) { 635 | return stringifyArrayReplacer('', value, [], getUniqueReplacerSet(replacer), spacer, '') 636 | } 637 | } 638 | if (spacer.length !== 0) { 639 | return stringifyIndent('', value, [], spacer, '') 640 | } 641 | } 642 | return stringifySimple('', value, []) 643 | } 644 | 645 | return stringify 646 | } 647 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safe-stable-stringify", 3 | "version": "2.5.0", 4 | "description": "Deterministic and safely JSON.stringify to quickly serialize JavaScript objects", 5 | "exports": { 6 | "require": "./index.js", 7 | "import": "./esm/wrapper.js" 8 | }, 9 | "keywords": [ 10 | "stable", 11 | "stringify", 12 | "JSON", 13 | "JSON.stringify", 14 | "safe", 15 | "serialize", 16 | "deterministic", 17 | "circular", 18 | "object", 19 | "predicable", 20 | "repeatable", 21 | "fast", 22 | "bigint" 23 | ], 24 | "main": "index.js", 25 | "scripts": { 26 | "test": "standard && tap test.js", 27 | "tap": "tap test.js", 28 | "tap:only": "tap test.js --watch --only", 29 | "benchmark": "node --allow-natives-syntax benchmark.js", 30 | "compare": "node --allow-natives-syntax compare.js", 31 | "lint": "standard --fix", 32 | "tsc": "tsc --project tsconfig.json" 33 | }, 34 | "engines": { 35 | "node": ">=10" 36 | }, 37 | "author": "Ruben Bridgewater", 38 | "license": "MIT", 39 | "typings": "index.d.ts", 40 | "devDependencies": { 41 | "@types/json-stable-stringify": "^1.0.34", 42 | "@types/node": "^18.11.18", 43 | "clone": "^2.1.2", 44 | "fast-json-stable-stringify": "^2.1.0", 45 | "fast-safe-stringify": "^2.1.1", 46 | "fast-stable-stringify": "^1.0.0", 47 | "faster-stable-stringify": "^1.0.0", 48 | "fastest-stable-stringify": "^2.0.2", 49 | "json-stable-stringify": "^1.0.1", 50 | "json-stringify-deterministic": "^1.0.7", 51 | "json-stringify-safe": "^5.0.1", 52 | "bench-node": "^0.5.4", 53 | "standard": "^16.0.4", 54 | "tap": "^15.0.9", 55 | "typescript": "^4.8.3" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/BridgeAR/safe-stable-stringify.git" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/BridgeAR/safe-stable-stringify/issues" 63 | }, 64 | "homepage": "https://github.com/BridgeAR/safe-stable-stringify#readme" 65 | } 66 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # safe-stable-stringify 2 | 3 | Safe, deterministic and fast serialization alternative to [JSON.stringify][]. 4 | Zero dependencies. ESM and CJS. 100% coverage. 5 | 6 | Gracefully handles circular structures and bigint instead of throwing. 7 | 8 | Optional custom circular values, deterministic behavior or strict JSON 9 | compatibility check. 10 | 11 | ## stringify(value[, replacer[, space]]) 12 | 13 | The same as [JSON.stringify][]. 14 | 15 | * `value` {any} 16 | * `replacer` {string[]|function|null} 17 | * `space` {number|string} 18 | * Returns: {string} 19 | 20 | ```js 21 | const stringify = require('safe-stable-stringify') 22 | 23 | const bigint = { a: 0, c: 2n, b: 1 } 24 | 25 | stringify(bigint) 26 | // '{"a":0,"b":1,"c":2}' 27 | JSON.stringify(bigint) 28 | // TypeError: Do not know how to serialize a BigInt 29 | 30 | const circular = { b: 1, a: 0 } 31 | circular.circular = circular 32 | 33 | stringify(circular) 34 | // '{"a":0,"b":1,"circular":"[Circular]"}' 35 | JSON.stringify(circular) 36 | // TypeError: Converting circular structure to JSON 37 | 38 | stringify(circular, ['a', 'b'], 2) 39 | // { 40 | // "a": 0, 41 | // "b": 1 42 | // } 43 | ``` 44 | 45 | ## stringify.configure(options) 46 | 47 | * `bigint` {boolean} If `true`, bigint values are converted to a number. 48 | Otherwise they are ignored. **Default:** `true`. 49 | * `circularValue` {string|null|undefined|ErrorConstructor} Defines the value for 50 | circular references. Set to `undefined`, circular properties are not 51 | serialized (array entries are replaced with `null`). Set to `Error`, to throw 52 | on circular references. **Default:** `'[Circular]'`. 53 | * `deterministic` {boolean|function} If `true` or a `Array#sort(comparator)` 54 | comparator method, guarantee a deterministic key order instead of relying on 55 | the insertion order. **Default:** `true`. 56 | * `maximumBreadth` {number} Maximum number of entries to serialize per object 57 | (at least one). The serialized output contains information about how many 58 | entries have not been serialized. Ignored properties are counted as well 59 | (e.g., properties with symbol values). Using the array replacer overrules this 60 | option. **Default:** `Infinity` 61 | * `maximumDepth` {number} Maximum number of object nesting levels (at least 1) 62 | that will be serialized. Objects at the maximum level are serialized as 63 | `'[Object]'` and arrays as `'[Array]'`. **Default:** `Infinity` 64 | * `strict` {boolean} Instead of handling any JSON value gracefully, throw an 65 | error in case it may not be represented as JSON (functions, NaN, ...). 66 | Circular values and bigint values throw as well in case either option is not 67 | explicitly defined. Sets and Maps are not detected as well as Symbol keys! 68 | **Default:** `false` 69 | * `safe` {boolean} If `true`, calls to .toJSON() and getters that throw an error 70 | are going to return the error message as content in place of the object 71 | instead of throwing the error. **Default:** `false` 72 | * Returns: {function} A stringify function with the options applied. 73 | 74 | ```js 75 | import { configure } from 'safe-stable-stringify' 76 | 77 | const stringify = configure({ 78 | bigint: true, 79 | circularValue: 'Magic circle!', 80 | deterministic: false, 81 | maximumDepth: 1, 82 | maximumBreadth: 4 83 | }) 84 | 85 | const circular = { 86 | bigint: 999_999_999_999_999_999n, 87 | typed: new Uint8Array(3), 88 | deterministic: "I don't think so", 89 | } 90 | circular.circular = circular 91 | circular.ignored = true 92 | circular.alsoIgnored = 'Yes!' 93 | 94 | const stringified = stringify(circular, null, 4) 95 | 96 | console.log(stringified) 97 | // { 98 | // "bigint": 999999999999999999, 99 | // "typed": "[Object]", 100 | // "deterministic": "I don't think so", 101 | // "circular": "Magic circle!", 102 | // "...": "2 items not stringified" 103 | // } 104 | 105 | const throwOnCircular = configure({ 106 | circularValue: Error 107 | }) 108 | 109 | throwOnCircular(circular); 110 | // TypeError: Converting circular structure to JSON 111 | ``` 112 | 113 | ## Differences to JSON.stringify 114 | 115 | 1. _Circular values_ are replaced with the string `[Circular]` (configurable). 116 | 1. _Object keys_ are sorted instead of using the insertion order (configurable). 117 | 1. _BigInt_ values are stringified as regular number instead of throwing a 118 | TypeError (configurable). 119 | 1. _Boxed primitives_ (e.g., `Number(5)`) are not unboxed and are handled as 120 | regular object. 121 | 122 | Those are the only differences to `JSON.stringify()`. This is a side effect free 123 | variant and [`toJSON`][], [`replacer`][] and the [`spacer`][] work the same as 124 | with `JSON.stringify()`. 125 | 126 | ## Performance / Benchmarks 127 | 128 | Currently this is by far the fastest known stable (deterministic) stringify 129 | implementation. This is especially important for big objects and TypedArrays. 130 | 131 | (Dell Precision 5540, i7-9850H CPU @ 2.60GHz, Node.js 16.11.1) 132 | 133 | ```md 134 | simple: simple object x 3,463,894 ops/sec ±0.44% (98 runs sampled) 135 | simple: circular x 1,236,007 ops/sec ±0.46% (99 runs sampled) 136 | simple: deep x 18,942 ops/sec ±0.41% (93 runs sampled) 137 | simple: deep circular x 18,690 ops/sec ±0.72% (96 runs sampled) 138 | 139 | replacer: simple object x 2,664,940 ops/sec ±0.31% (98 runs sampled) 140 | replacer: circular x 1,015,981 ops/sec ±0.09% (99 runs sampled) 141 | replacer: deep x 17,328 ops/sec ±0.38% (97 runs sampled) 142 | replacer: deep circular x 17,071 ops/sec ±0.21% (98 runs sampled) 143 | 144 | array: simple object x 3,869,608 ops/sec ±0.22% (98 runs sampled) 145 | array: circular x 3,853,943 ops/sec ±0.45% (96 runs sampled) 146 | array: deep x 3,563,227 ops/sec ±0.20% (100 runs sampled) 147 | array: deep circular x 3,286,475 ops/sec ±0.07% (100 runs sampled) 148 | 149 | indentation: simple object x 2,183,162 ops/sec ±0.66% (97 runs sampled) 150 | indentation: circular x 872,538 ops/sec ±0.57% (98 runs sampled) 151 | indentation: deep x 16,795 ops/sec ±0.48% (93 runs sampled) 152 | indentation: deep circular x 16,443 ops/sec ±0.40% (97 runs sampled) 153 | ``` 154 | 155 | Comparing `safe-stable-stringify` with known alternatives: 156 | 157 | ```md 158 | fast-json-stable-stringify x 18,765 ops/sec ±0.71% (94 runs sampled) 159 | json-stable-stringify x 13,870 ops/sec ±0.72% (94 runs sampled) 160 | fast-stable-stringify x 21,343 ops/sec ±0.33% (95 runs sampled) 161 | faster-stable-stringify x 17,707 ops/sec ±0.44% (97 runs sampled) 162 | json-stringify-deterministic x 11,208 ops/sec ±0.57% (98 runs sampled) 163 | fast-safe-stringify x 21,460 ops/sec ±0.75% (99 runs sampled) 164 | this x 30,367 ops/sec ±0.39% (96 runs sampled) 165 | 166 | The fastest is this 167 | ``` 168 | 169 | The `fast-safe-stringify` comparison uses the modules stable implementation. 170 | 171 | ## Acknowledgements 172 | 173 | Sponsored by [MaibornWolff](https://www.maibornwolff.de/) and [nearForm](http://nearform.com) 174 | 175 | ## License 176 | 177 | MIT 178 | 179 | [`replacer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The%20replacer%20parameter 180 | [`spacer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The%20space%20argument 181 | [`toJSON`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior 182 | [JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify 183 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('tap') 2 | const { stringify } = require('./index.js') 3 | const clone = require('clone') 4 | 5 | test('toJSON receives array keys as string', function (assert) { 6 | const object = [{ 7 | toJSON (key) { 8 | assert.equal(key, '0') 9 | return 42 10 | } 11 | }] 12 | 13 | const expected = JSON.stringify(object) 14 | 15 | let actual = stringify(object) 16 | assert.equal(actual, expected) 17 | 18 | actual = stringify(object, ['0']) 19 | assert.equal(actual, expected) 20 | 21 | // @ts-expect-error 22 | actual = stringify(object, (key, value) => value) 23 | assert.equal(actual, expected) 24 | 25 | actual = stringify(object, null, 2) 26 | assert.equal(actual, '[\n 42\n]') 27 | 28 | assert.end() 29 | }) 30 | 31 | test('circular reference to root', function (assert) { 32 | const fixture = { name: 'Tywin Lannister' } 33 | fixture.circle = fixture 34 | const expected = JSON.stringify( 35 | { circle: '[Circular]', name: 'Tywin Lannister' } 36 | ) 37 | const actual = stringify(fixture) 38 | assert.equal(actual, expected) 39 | assert.end() 40 | }) 41 | 42 | test('nested circular reference to root', function (assert) { 43 | const fixture = { name: 'Tywin\n\t"Lannister' } 44 | fixture.id = { circle: fixture } 45 | const expected = JSON.stringify( 46 | { id: { circle: '[Circular]' }, name: 'Tywin\n\t"Lannister' } 47 | ) 48 | const actual = stringify(fixture) 49 | assert.equal(actual, expected) 50 | assert.end() 51 | }) 52 | 53 | test('throw if circularValue is set to TypeError', function (assert) { 54 | const noCircularStringify = stringify.configure({ circularValue: TypeError }) 55 | const object = { number: 42, boolean: true, string: 'Yes!' } 56 | object.circular = object 57 | 58 | assert.throws(() => noCircularStringify(object), TypeError) 59 | assert.end() 60 | }) 61 | 62 | test('throw if circularValue is set to Error', function (assert) { 63 | const noCircularStringify = stringify.configure({ circularValue: Error }) 64 | const object = { number: 42, boolean: true, string: 'Yes!' } 65 | object.circular = object 66 | 67 | assert.throws(() => noCircularStringify(object), TypeError) 68 | assert.end() 69 | }) 70 | 71 | test('child circular reference', function (assert) { 72 | const fixture = { name: 'Tywin Lannister', child: { name: 'Tyrion\n\t"Lannister'.repeat(20) } } 73 | fixture.child.dinklage = fixture.child 74 | const expected = JSON.stringify({ 75 | child: { 76 | dinklage: '[Circular]', name: 'Tyrion\n\t"Lannister'.repeat(20) 77 | }, 78 | name: 'Tywin Lannister' 79 | }) 80 | const actual = stringify(fixture) 81 | assert.equal(actual, expected) 82 | assert.end() 83 | }) 84 | 85 | test('nested child circular reference', function (assert) { 86 | const fixture = { name: 'Tywin Lannister', child: { name: 'Tyrion Lannister' } } 87 | fixture.child.actor = { dinklage: fixture.child } 88 | const expected = JSON.stringify({ 89 | child: { 90 | actor: { dinklage: '[Circular]' }, name: 'Tyrion Lannister' 91 | }, 92 | name: 'Tywin Lannister' 93 | }) 94 | const actual = stringify(fixture) 95 | assert.equal(actual, expected) 96 | assert.end() 97 | }) 98 | 99 | test('circular objects in an array', function (assert) { 100 | const fixture = { name: 'Tywin Lannister' } 101 | fixture.hand = [fixture, fixture] 102 | const expected = JSON.stringify({ 103 | hand: ['[Circular]', '[Circular]'], name: 'Tywin Lannister' 104 | }) 105 | const actual = stringify(fixture) 106 | assert.equal(actual, expected) 107 | assert.end() 108 | }) 109 | 110 | test('nested circular references in an array', function (assert) { 111 | const fixture = { 112 | name: 'Tywin Lannister', 113 | offspring: [{ name: 'Tyrion Lannister' }, { name: 'Cersei Lannister' }] 114 | } 115 | fixture.offspring[0].dinklage = fixture.offspring[0] 116 | fixture.offspring[1].headey = fixture.offspring[1] 117 | 118 | const expected = JSON.stringify({ 119 | name: 'Tywin Lannister', 120 | offspring: [ 121 | { dinklage: '[Circular]', name: 'Tyrion Lannister' }, 122 | { headey: '[Circular]', name: 'Cersei Lannister' } 123 | ] 124 | }) 125 | const actual = stringify(fixture) 126 | assert.equal(actual, expected) 127 | assert.end() 128 | }) 129 | 130 | test('circular arrays', function (assert) { 131 | const fixture = [] 132 | fixture.push(fixture, fixture) 133 | const expected = JSON.stringify(['[Circular]', '[Circular]']) 134 | const actual = stringify(fixture) 135 | assert.equal(actual, expected) 136 | assert.end() 137 | }) 138 | 139 | test('nested circular arrays', function (assert) { 140 | const fixture = [] 141 | fixture.push( 142 | { name: 'Jon Snow', circular: fixture }, 143 | { name: 'Ramsay Bolton', circular: fixture } 144 | ) 145 | const expected = JSON.stringify([ 146 | { circular: '[Circular]', name: 'Jon Snow' }, 147 | { circular: '[Circular]', name: 'Ramsay Bolton' } 148 | ]) 149 | const actual = stringify(fixture) 150 | assert.equal(actual, expected) 151 | assert.end() 152 | }) 153 | 154 | test('repeated non-circular references in objects', function (assert) { 155 | const daenerys = { name: 'Daenerys Targaryen' } 156 | const fixture = { 157 | motherOfDragons: daenerys, 158 | queenOfMeereen: daenerys 159 | } 160 | const expected = JSON.stringify(fixture) 161 | const actual = stringify(fixture) 162 | assert.equal(actual, expected) 163 | assert.end() 164 | }) 165 | 166 | test('repeated non-circular references in arrays', function (assert) { 167 | const daenerys = { name: 'Daenerys Targaryen' } 168 | const fixture = [daenerys, daenerys] 169 | const expected = JSON.stringify(fixture) 170 | const actual = stringify(fixture) 171 | assert.equal(actual, expected) 172 | assert.end() 173 | }) 174 | 175 | test('double child circular reference', function (assert) { 176 | // create circular reference 177 | const child = { name: 'Tyrion Lannister' } 178 | child.dinklage = child 179 | 180 | // include it twice in the fixture 181 | const fixture = { name: 'Tywin Lannister', childA: child, childB: child } 182 | const cloned = clone(fixture) 183 | const expected = JSON.stringify({ 184 | childA: { 185 | dinklage: '[Circular]', name: 'Tyrion Lannister' 186 | }, 187 | childB: { 188 | dinklage: '[Circular]', name: 'Tyrion Lannister' 189 | }, 190 | name: 'Tywin Lannister' 191 | }) 192 | const actual = stringify(fixture) 193 | assert.equal(actual, expected) 194 | 195 | // check if the fixture has not been modified 196 | assert.same(fixture, cloned) 197 | assert.end() 198 | }) 199 | 200 | test('child circular reference with toJSON', function (assert) { 201 | // Create a test object that has an overridden `toJSON` property 202 | TestObject.prototype.toJSON = function () { return { special: 'case' } } 203 | function TestObject () {} 204 | 205 | // Creating a simple circular object structure 206 | const parentObject = {} 207 | parentObject.childObject = new TestObject() 208 | // @ts-expect-error 209 | parentObject.childObject.parentObject = parentObject 210 | 211 | // Creating a simple circular object structure 212 | const otherParentObject = new TestObject() 213 | // @ts-expect-error 214 | otherParentObject.otherChildObject = {} 215 | // @ts-expect-error 216 | otherParentObject.otherChildObject.otherParentObject = otherParentObject 217 | 218 | // Making sure our original tests work 219 | // @ts-expect-error 220 | assert.same(parentObject.childObject.parentObject, parentObject) 221 | // @ts-expect-error 222 | assert.same(otherParentObject.otherChildObject.otherParentObject, otherParentObject) 223 | 224 | // Should both be idempotent 225 | assert.equal(stringify(parentObject), '{"childObject":{"special":"case"}}') 226 | assert.equal(stringify(otherParentObject), '{"special":"case"}') 227 | 228 | // Therefore the following assertion should be `true` 229 | // @ts-expect-error 230 | assert.same(parentObject.childObject.parentObject, parentObject) 231 | // @ts-expect-error 232 | assert.same(otherParentObject.otherChildObject.otherParentObject, otherParentObject) 233 | 234 | assert.end() 235 | }) 236 | 237 | test('null object', function (assert) { 238 | const expected = JSON.stringify(null) 239 | const actual = stringify(null) 240 | assert.equal(actual, expected) 241 | assert.end() 242 | }) 243 | 244 | test('null property', function (assert) { 245 | const obj = { f: null } 246 | const expected = JSON.stringify(obj) 247 | const actual = stringify(obj) 248 | assert.equal(actual, expected) 249 | assert.end() 250 | }) 251 | 252 | test('null property', function (assert) { 253 | const obj = { toJSON () { return null } } 254 | const expected = JSON.stringify(obj) 255 | const actual = stringify(obj) 256 | assert.equal(actual, expected) 257 | assert.end() 258 | }) 259 | 260 | test('nested child circular reference in toJSON', function (assert) { 261 | const circle = { some: 'data' } 262 | circle.circle = circle 263 | const a = { 264 | b: { 265 | toJSON: function () { 266 | // @ts-expect-error 267 | a.b = 2 268 | return '[Redacted]' 269 | } 270 | }, 271 | baz: { 272 | circle, 273 | toJSON: function () { 274 | // @ts-expect-error 275 | a.baz = circle 276 | return '[Redacted]' 277 | } 278 | } 279 | } 280 | const o = { 281 | a, 282 | bar: a 283 | } 284 | 285 | const expected = JSON.stringify({ 286 | a: { 287 | b: '[Redacted]', 288 | baz: '[Redacted]' 289 | }, 290 | bar: { 291 | b: 2, 292 | baz: { 293 | circle: '[Circular]', 294 | some: 'data' 295 | } 296 | } 297 | }) 298 | const actual = stringify(o) 299 | assert.equal(actual, expected) 300 | assert.end() 301 | }) 302 | 303 | test('invalid replacer being ignored', function (assert) { 304 | const obj = { a: true } 305 | 306 | // @ts-expect-error 307 | const actual = stringify(obj, 'invalidReplacer') 308 | // @ts-expect-error 309 | const expected = stringify(obj, 'invalidReplacer') 310 | assert.equal(actual, expected) 311 | 312 | assert.end() 313 | }) 314 | 315 | test('replacer removing elements', function (assert) { 316 | const replacer = function (k, v) { 317 | assert.type(k, 'string') 318 | if (k === 'remove') return 319 | if (k === '0') typedkeysInReplacer = true 320 | return v 321 | } 322 | const obj = { f: null, remove: true, typed: new Int32Array(1) } 323 | 324 | let typedkeysInReplacer = false 325 | const expected = JSON.stringify(obj, replacer) 326 | assert.ok(typedkeysInReplacer) 327 | typedkeysInReplacer = false 328 | 329 | let actual = stringify(obj, replacer) 330 | assert.ok(typedkeysInReplacer) 331 | typedkeysInReplacer = false 332 | 333 | assert.equal(actual, expected) 334 | 335 | obj.obj = obj 336 | actual = stringify(obj, replacer) 337 | assert.equal(actual, '{"f":null,"obj":"[Circular]","typed":{"0":0}}') 338 | 339 | assert.end() 340 | }) 341 | 342 | test('replacer removing elements and indentation', function (assert) { 343 | const replacer = function (k, v) { 344 | if (k === 'remove') return 345 | return v 346 | } 347 | const obj = { f: null, remove: true } 348 | const expected = JSON.stringify(obj, replacer, 2) 349 | const actual = stringify(obj, replacer, 2) 350 | assert.equal(actual, expected) 351 | assert.end() 352 | }) 353 | 354 | test('replacer removing all elements', function (assert) { 355 | const replacer = function (k, v) { 356 | assert.type(k, 'string') 357 | if (k !== '') return 358 | return k 359 | } 360 | const obj = [{ f: null, remove: true }] 361 | let expected = JSON.stringify(obj, replacer) 362 | let actual = stringify(obj, replacer) 363 | assert.equal(actual, expected) 364 | 365 | expected = JSON.stringify({ toJSON () { return obj } }, replacer) 366 | actual = stringify({ toJSON () { return obj } }, replacer) 367 | assert.equal(actual, expected) 368 | 369 | assert.end() 370 | }) 371 | 372 | test('replacer removing all elements and indentation', function (assert) { 373 | const replacer = function (k, v) { 374 | if (k !== '') return 375 | return k 376 | } 377 | const obj = [{ f: null, remove: true }] 378 | const expected = JSON.stringify(obj, replacer, 2) 379 | const actual = stringify(obj, replacer, 2) 380 | assert.equal(actual, expected) 381 | assert.end() 382 | }) 383 | 384 | test('array replacer', function (assert) { 385 | const replacer = ['f', 1, null] 386 | const obj = { f: null, null: true, 1: false } 387 | // The null element will be ignored! 388 | // @ts-expect-error 389 | const expected = JSON.stringify(obj, replacer) 390 | // @ts-expect-error 391 | let actual = stringify(obj, replacer) 392 | assert.equal(actual, expected) 393 | 394 | // @ts-expect-error 395 | obj.f = obj 396 | 397 | // @ts-expect-error 398 | actual = stringify({ toJSON () { return obj } }, replacer) 399 | assert.equal(actual, expected.replace('null', '"[Circular]"')) 400 | 401 | assert.end() 402 | }) 403 | 404 | test('empty array replacer', function (assert) { 405 | const replacer = [] 406 | const obj = { f: null, null: true, 1: false } 407 | // The null element will be removed! 408 | const expected = JSON.stringify(obj, replacer) 409 | const actual = stringify(obj, replacer) 410 | assert.equal(actual, expected) 411 | 412 | assert.end() 413 | }) 414 | 415 | test('array replacer and indentation', function (assert) { 416 | const replacer = ['f', 1, null] 417 | const obj = { f: null, null: true, 1: [false, -Infinity, 't'] } 418 | // The null element will be removed! 419 | // @ts-expect-error 420 | const expected = JSON.stringify(obj, replacer, 2) 421 | // @ts-expect-error 422 | const actual = stringify(obj, replacer, 2) 423 | assert.equal(actual, expected) 424 | assert.end() 425 | }) 426 | 427 | test('indent zero', function (assert) { 428 | const obj = { f: null, null: true, 1: false } 429 | const expected = JSON.stringify(obj, null, 0) 430 | const actual = stringify(obj, null, 0) 431 | assert.equal(actual, expected) 432 | assert.end() 433 | }) 434 | 435 | test('replacer and indentation without match', function (assert) { 436 | const replacer = function (k, v) { 437 | if (k === '') return v 438 | } 439 | const obj = { f: 1, b: null, c: 't', d: Infinity, e: true } 440 | const expected = JSON.stringify(obj, replacer, ' ') 441 | const actual = stringify(obj, replacer, ' ') 442 | assert.equal(actual, expected) 443 | assert.end() 444 | }) 445 | 446 | test('array replacer and indentation without match', function (assert) { 447 | const replacer = [''] 448 | const obj = { f: 1, b: null, c: 't', d: Infinity, e: true } 449 | const expected = JSON.stringify(obj, replacer, ' ') 450 | const actual = stringify(obj, replacer, ' ') 451 | assert.equal(actual, expected) 452 | assert.end() 453 | }) 454 | 455 | test('indentation without match', function (assert) { 456 | const obj = { f: undefined } 457 | const expected = JSON.stringify(obj, undefined, 3) 458 | const actual = stringify(obj, undefined, 3) 459 | assert.equal(actual, expected) 460 | assert.end() 461 | }) 462 | 463 | test('array nulls and indentation', function (assert) { 464 | const obj = [null, null] 465 | const expected = JSON.stringify(obj, undefined, 3) 466 | const actual = stringify(obj, undefined, 3) 467 | assert.equal(actual, expected) 468 | assert.end() 469 | }) 470 | 471 | test('array nulls, replacer and indentation', function (assert) { 472 | const obj = [null, Infinity, 5, true, false] 473 | const expected = JSON.stringify(obj, (_, v) => v, 3) 474 | const actual = stringify(obj, (_, v) => v, 3) 475 | assert.equal(actual, expected) 476 | assert.end() 477 | }) 478 | 479 | test('array nulls and replacer', function (assert) { 480 | const obj = [null, Infinity, 5, true, false, [], {}] 481 | const expected = JSON.stringify(obj, (_, v) => v) 482 | const actual = stringify(obj, (_, v) => v) 483 | assert.equal(actual, expected) 484 | assert.end() 485 | }) 486 | 487 | test('array nulls, array replacer and indentation', function (assert) { 488 | const obj = [null, null, [], {}] 489 | // @ts-expect-error 490 | const expected = JSON.stringify(obj, [false], 3) 491 | // @ts-expect-error 492 | const actual = stringify(obj, [false], 3) 493 | assert.equal(actual, expected) 494 | assert.end() 495 | }) 496 | 497 | test('array and array replacer', function (assert) { 498 | const obj = [null, null, 't', Infinity, true, false, [], {}] 499 | const expected = JSON.stringify(obj, [2]) 500 | const actual = stringify(obj, [2]) 501 | assert.equal(actual, expected) 502 | assert.end() 503 | }) 504 | 505 | test('indentation with elements', function (assert) { 506 | const obj = { a: 1, b: [null, 't', Infinity, true] } 507 | const expected = JSON.stringify(obj, null, 5) 508 | const actual = stringify(obj, null, 5) 509 | assert.equal(actual, expected) 510 | assert.end() 511 | }) 512 | 513 | test('object with undefined values', function (assert) { 514 | let obj = { a: 1, c: undefined, b: 'hello', d: [], e: {} } 515 | 516 | let expected = JSON.stringify(obj) 517 | let actual = stringify(obj) 518 | assert.equal(actual, expected) 519 | 520 | // @ts-expect-error 521 | obj = { b: 'hello', a: undefined, c: 1 } 522 | 523 | expected = JSON.stringify(obj) 524 | actual = stringify(obj) 525 | assert.equal(actual, expected) 526 | 527 | assert.end() 528 | }) 529 | 530 | test('undefined values and indented', function (assert) { 531 | const obj1 = { a: 1, c: undefined, b: 'hello' } 532 | 533 | let expected = JSON.stringify(obj1, null, 2) 534 | let actual = stringify(obj1, null, 2) 535 | assert.equal(actual, expected) 536 | 537 | const obj2 = { b: 'hello', a: undefined, c: 1 } 538 | 539 | expected = JSON.stringify(obj2) 540 | actual = stringify(obj2) 541 | assert.equal(actual, expected) 542 | 543 | assert.end() 544 | }) 545 | 546 | test('bigint option', function (assert) { 547 | const stringifyNoBigInt = stringify.configure({ bigint: false }) 548 | const stringifyBigInt = stringify.configure({ bigint: true }) 549 | 550 | const obj = { a: 1n } 551 | const actualBigInt = stringifyBigInt(obj, null, 1) 552 | const actualNoBigInt = stringifyNoBigInt(obj, null, 1) 553 | const actualDefault = stringify(obj, null, 1) 554 | const expectedBigInt = '{\n "a": 1\n}' 555 | const expectedNoBigInt = '{}' 556 | 557 | assert.equal(actualNoBigInt, expectedNoBigInt) 558 | assert.throws(() => JSON.stringify(obj, null, 1), TypeError) 559 | 560 | assert.equal(actualBigInt, expectedBigInt) 561 | assert.equal(actualDefault, expectedBigInt) 562 | 563 | // @ts-expect-error 564 | assert.throws(() => stringify.configure({ bigint: null }), /bigint/) 565 | 566 | assert.end() 567 | }) 568 | 569 | test('bigint option with replacer', function (assert) { 570 | const stringifyBigInt = stringify.configure({ bigint: true }) 571 | 572 | const obj = { a: new BigUint64Array([1n]), 0: 1n } 573 | const actualArrayReplacer = stringifyBigInt(obj, ['0', 'a']) 574 | const actualFnReplacer = stringifyBigInt(obj, (k, v) => v) 575 | const expected = '{"0":1,"a":{"0":1}}' 576 | 577 | assert.equal(actualArrayReplacer, expected) 578 | assert.equal(actualFnReplacer, expected) 579 | 580 | assert.end() 581 | }) 582 | 583 | test('bigint and typed array with indentation', function (assert) { 584 | const obj = { a: 1n, t: new Int8Array(1) } 585 | const expected = '{\n "a": 1,\n "t": {\n "0": 0\n }\n}' 586 | const actual = stringify(obj, null, 1) 587 | assert.equal(actual, expected) 588 | assert.end() 589 | }) 590 | 591 | test('bigint and typed array without indentation', function (assert) { 592 | const obj = { a: 1n, t: new Int8Array(1) } 593 | const expected = '{"a":1,"t":{"0":0}}' 594 | const actual = stringify(obj, null, 0) 595 | assert.equal(actual, expected) 596 | assert.end() 597 | }) 598 | 599 | test('no bigint without indentation', function (assert) { 600 | const stringifyNoBigInt = stringify.configure({ bigint: false }) 601 | const obj = { a: 1n, t: new Int8Array(1) } 602 | const expected = '{"t":{"0":0}}' 603 | const actual = stringifyNoBigInt(obj, null, 0) 604 | assert.equal(actual, expected) 605 | assert.end() 606 | }) 607 | 608 | test('circular value option should allow strings and null', function (assert) { 609 | let stringifyCircularValue = stringify.configure({ circularValue: 'YEAH!!!' }) 610 | 611 | const obj = {} 612 | obj.circular = obj 613 | 614 | const expected = '{"circular":"YEAH!!!"}' 615 | const actual = stringifyCircularValue(obj) 616 | assert.equal(actual, expected) 617 | assert.equal(stringify(obj), '{"circular":"[Circular]"}') 618 | 619 | stringifyCircularValue = stringify.configure({ circularValue: null }) 620 | assert.equal(stringifyCircularValue(obj), '{"circular":null}') 621 | 622 | assert.end() 623 | }) 624 | 625 | test('circular value option should throw for invalid values', function (assert) { 626 | // @ts-expect-error 627 | assert.throws(() => stringify.configure({ circularValue: { objects: 'are not allowed' } }), /circularValue/) 628 | 629 | assert.end() 630 | }) 631 | 632 | test('circular value option set to undefined should skip serialization', function (assert) { 633 | const stringifyCircularValue = stringify.configure({ circularValue: undefined }) 634 | 635 | const obj = { a: 1 } 636 | obj.circular = obj 637 | obj.b = [2, obj] 638 | 639 | const expected = '{"a":1,"b":[2,null]}' 640 | const actual = stringifyCircularValue(obj) 641 | assert.equal(actual, expected) 642 | 643 | assert.end() 644 | }) 645 | 646 | test('non-deterministic', function (assert) { 647 | const stringifyNonDeterministic = stringify.configure({ deterministic: false }) 648 | 649 | const obj = { b: true, a: false } 650 | 651 | const expected = JSON.stringify(obj) 652 | const actual = stringifyNonDeterministic(obj) 653 | assert.equal(actual, expected) 654 | 655 | // @ts-expect-error 656 | assert.throws(() => stringify.configure({ deterministic: 1 }), /deterministic/) 657 | 658 | assert.end() 659 | }) 660 | 661 | test('non-deterministic with replacer', function (assert) { 662 | const stringifyNonDeterministic = stringify.configure({ deterministic: false, bigint: false }) 663 | 664 | const obj = { b: true, a: 5n, c: Infinity, d: 4, e: [Symbol('null'), 5, Symbol('null')] } 665 | const keys = Object.keys(obj) 666 | 667 | const expected = stringify(obj, ['b', 'c', 'd', 'e']) 668 | const actualA = stringifyNonDeterministic(obj, keys) 669 | assert.equal(actualA, expected) 670 | 671 | const actualB = stringifyNonDeterministic(obj, (k, v) => v) 672 | assert.equal(actualB, expected) 673 | 674 | assert.end() 675 | }) 676 | 677 | test('non-deterministic with indentation', function (assert) { 678 | const stringifyNonDeterministic = stringify.configure({ deterministic: false, bigint: false }) 679 | 680 | const obj = { b: true, a: 5, c: Infinity, d: false, e: [Symbol('null'), 5, Symbol('null')] } 681 | 682 | const expected = JSON.stringify(obj, null, 1) 683 | const actual = stringifyNonDeterministic(obj, null, 1) 684 | assert.equal(actual, expected) 685 | 686 | assert.end() 687 | }) 688 | 689 | test('check typed arrays', function (assert) { 690 | const obj = [null, null, new Float32Array(99), Infinity, Symbol('null'), true, false, [], {}, Symbol('null')] 691 | const expected = JSON.stringify(obj) 692 | const actual = stringify(obj) 693 | assert.equal(actual, expected) 694 | assert.end() 695 | }) 696 | 697 | test('check small typed arrays with extra properties', function (assert) { 698 | const obj = new Uint8Array(0) 699 | // @ts-expect-error 700 | obj.foo = true 701 | let expected = JSON.stringify(obj) 702 | const actualA = stringify(obj) 703 | assert.equal(actualA, expected) 704 | 705 | expected = JSON.stringify(obj, null, 2) 706 | const actualB = stringify(obj, null, 2) 707 | assert.equal(actualB, expected) 708 | 709 | expected = JSON.stringify(obj, ['foo']) 710 | const actualC = stringify(obj, ['foo']) 711 | assert.equal(actualC, expected) 712 | 713 | expected = JSON.stringify(obj, (a, b) => b) 714 | const actualD = stringify(obj, (a, b) => b) 715 | assert.equal(actualD, expected) 716 | 717 | assert.end() 718 | }) 719 | 720 | test('trigger sorting fast path for objects with lots of properties', function (assert) { 721 | const keys = [] 722 | const obj = {} 723 | for (let i = 0; i < 1e4; i++) { 724 | obj[`a${i}`] = i 725 | keys.push(`a${i}`) 726 | } 727 | 728 | const start = Date.now() 729 | 730 | stringify(obj) 731 | assert.ok(Date.now() - start < 100) 732 | const now = Date.now() 733 | const actualTime = now - start 734 | keys.sort() 735 | const expectedTime = Date.now() - now 736 | assert.ok(Math.abs(actualTime - expectedTime) < 50) 737 | assert.end() 738 | }) 739 | 740 | test('maximum spacer length', function (assert) { 741 | const input = { a: 0 } 742 | const expected = `{\n${' '.repeat(10)}"a": 0\n}` 743 | assert.equal(stringify(input, null, 11), expected) 744 | assert.equal(stringify(input, null, 1e5), expected) 745 | assert.equal(stringify(input, null, ' '.repeat(11)), expected) 746 | assert.equal(stringify(input, null, ' '.repeat(1e3)), expected) 747 | assert.end() 748 | }) 749 | 750 | test('indent properly; regression test for issue #16', function (assert) { 751 | const o = { 752 | collections: {}, 753 | config: { 754 | label: 'Some\ttext\t', 755 | options: { toJSON () { return { exportNotes: true } } }, 756 | preferences: [] 757 | }, 758 | items: [{ 759 | creators: [{ lastName: 'Lander' }, { toJSON () { return null } }], 760 | date: { toJSON () { return '01/01/1989' } } 761 | }] 762 | } 763 | 764 | const arrayReplacer = ['config', 'items', 'options', 'circular', 'preferences', 'creators'] 765 | 766 | const indentedJSON = JSON.stringify(o, null, 2) 767 | const indentedJSONArrayReplacer = JSON.stringify(o, arrayReplacer, 2) 768 | const indentedJSONArrayEmpty = JSON.stringify(o, [], 2) 769 | const indentedJSONReplacer = JSON.stringify(o, (k, v) => v, 2) 770 | 771 | assert.equal( 772 | stringify(o, null, 2), 773 | indentedJSON 774 | ) 775 | assert.equal( 776 | stringify(o, arrayReplacer, 2), 777 | indentedJSONArrayReplacer 778 | ) 779 | assert.equal( 780 | stringify(o, [], 2), 781 | indentedJSONArrayEmpty 782 | ) 783 | assert.equal( 784 | stringify(o, (k, v) => v, 2), 785 | indentedJSONReplacer 786 | ) 787 | 788 | o.items[0].circular = o 789 | 790 | const circularReplacement = '"items": [\n {\n "circular": "[Circular]",\n' 791 | const circularIdentifier = '"items": [\n {\n' 792 | 793 | assert.equal( 794 | stringify(o, arrayReplacer, 2), 795 | indentedJSONArrayReplacer.replace(circularIdentifier, circularReplacement) 796 | ) 797 | assert.equal( 798 | stringify(o, null, 2), 799 | indentedJSON.replace(circularIdentifier, circularReplacement) 800 | ) 801 | assert.equal( 802 | stringify(o, (k, v) => v, 2), 803 | indentedJSONReplacer.replace(circularIdentifier, circularReplacement) 804 | ) 805 | 806 | assert.end() 807 | }) 808 | 809 | test('should stop if max depth is reached', (assert) => { 810 | const serialize = stringify.configure({ 811 | maximumDepth: 5 812 | }) 813 | const nested = {} 814 | const MAX_DEPTH = 10 815 | let currentNestedObject = null 816 | for (let i = 0; i < MAX_DEPTH; i++) { 817 | const k = 'nest_' + i 818 | if (!currentNestedObject) { 819 | currentNestedObject = nested 820 | } 821 | currentNestedObject[k] = { 822 | foo: 'bar' 823 | } 824 | currentNestedObject = currentNestedObject[k] 825 | } 826 | const res = serialize(nested) 827 | assert.ok(res.indexOf('"nest_4":"[Object]"')) 828 | assert.end() 829 | }) 830 | 831 | test('should serialize only first 10 elements', (assert) => { 832 | const serialize = stringify.configure({ 833 | maximumBreadth: 10 834 | }) 835 | const breadth = {} 836 | const MAX_BREADTH = 100 837 | for (let i = 0; i < MAX_BREADTH; i++) { 838 | const k = 'key_' + i 839 | breadth[k] = 'foobar' 840 | } 841 | const res = serialize(breadth) 842 | const expected = '{"key_0":"foobar","key_1":"foobar","key_10":"foobar","key_11":"foobar","key_12":"foobar","key_13":"foobar","key_14":"foobar","key_15":"foobar","key_16":"foobar","key_17":"foobar","...":"90 items not stringified"}' 843 | assert.equal(res, expected) 844 | assert.end() 845 | }) 846 | 847 | test('should serialize only first 10 elements with custom replacer and indentation', (assert) => { 848 | const serialize = stringify.configure({ 849 | maximumBreadth: 10, 850 | maximumDepth: 1 851 | }) 852 | const breadth = { a: Array.from({ length: 100 }, (_, i) => i) } 853 | const MAX_BREADTH = 100 854 | for (let i = 0; i < MAX_BREADTH; i++) { 855 | const k = 'key_' + i 856 | breadth[k] = 'foobar' 857 | } 858 | const res = serialize(breadth, (k, v) => v, 2) 859 | const expected = `{ 860 | "a": "[Array]", 861 | "key_0": "foobar", 862 | "key_1": "foobar", 863 | "key_10": "foobar", 864 | "key_11": "foobar", 865 | "key_12": "foobar", 866 | "key_13": "foobar", 867 | "key_14": "foobar", 868 | "key_15": "foobar", 869 | "key_16": "foobar", 870 | "...": "91 items not stringified" 871 | }` 872 | assert.equal(res, expected) 873 | assert.end() 874 | }) 875 | 876 | test('maximumDepth config', function (assert) { 877 | const obj = { a: { b: { c: 1 }, a: [1, 2, 3] } } 878 | 879 | const serialize = stringify.configure({ 880 | maximumDepth: 2 881 | }) 882 | 883 | const result = serialize(obj, (key, val) => val) 884 | assert.equal(result, '{"a":{"a":"[Array]","b":"[Object]"}}') 885 | 886 | const res2 = serialize(obj, ['a', 'b']) 887 | assert.equal(res2, '{"a":{"a":"[Array]","b":{}}}') 888 | 889 | const json = JSON.stringify(obj, ['a', 'b']) 890 | assert.equal(json, '{"a":{"a":[1,2,3],"b":{}}}') 891 | 892 | const res3 = serialize(obj, null, 2) 893 | assert.equal(res3, `{ 894 | "a": { 895 | "a": "[Array]", 896 | "b": "[Object]" 897 | } 898 | }`) 899 | 900 | const res4 = serialize(obj) 901 | assert.equal(res4, '{"a":{"a":"[Array]","b":"[Object]"}}') 902 | 903 | assert.end() 904 | }) 905 | 906 | test('maximumBreadth config', function (assert) { 907 | const obj = { a: ['a', 'b', 'c', 'd', 'e'] } 908 | 909 | const serialize = stringify.configure({ 910 | maximumBreadth: 3 911 | }) 912 | 913 | const result = serialize(obj, (key, val) => val) 914 | assert.equal(result, '{"a":["a","b","c","... 1 item not stringified"]}') 915 | 916 | const res2 = serialize(obj, ['a', 'b']) 917 | assert.equal(res2, '{"a":["a","b","c","... 1 item not stringified"]}') 918 | 919 | const res3 = serialize(obj, null, 2) 920 | assert.equal(res3, `{ 921 | "a": [ 922 | "a", 923 | "b", 924 | "c", 925 | "... 1 item not stringified" 926 | ] 927 | }`) 928 | 929 | const res4 = serialize({ a: { a: 1, b: 1, c: 1, d: 1, e: 1 } }, null, 2) 930 | assert.equal(res4, `{ 931 | "a": { 932 | "a": 1, 933 | "b": 1, 934 | "c": 1, 935 | "...": "2 items not stringified" 936 | } 937 | }`) 938 | 939 | assert.end() 940 | }) 941 | test('limit number of keys with array replacer', function (assert) { 942 | const replacer = ['a', 'b', 'c', 'd', 'e'] 943 | const obj = { 944 | a: 'a', 945 | b: 'b', 946 | c: 'c', 947 | d: 'd', 948 | e: 'e', 949 | f: 'f', 950 | g: 'g', 951 | h: 'h' 952 | } 953 | 954 | const serialize = stringify.configure({ 955 | maximumBreadth: 3 956 | }) 957 | const res = serialize(obj, replacer, 2) 958 | const expected = `{ 959 | "a": "a", 960 | "b": "b", 961 | "c": "c", 962 | "d": "d", 963 | "e": "e" 964 | }` 965 | assert.equal(res, expected) 966 | assert.end() 967 | }) 968 | 969 | test('limit number of keys in array', (assert) => { 970 | const serialize = stringify.configure({ 971 | maximumBreadth: 3 972 | }) 973 | const arr = [] 974 | const MAX_BREADTH = 100 975 | for (let i = 0; i < MAX_BREADTH; i++) { 976 | arr.push(i) 977 | } 978 | const res = serialize(arr) 979 | const expected = '[0,1,2,"... 96 items not stringified"]' 980 | assert.equal(res, expected) 981 | assert.end() 982 | }) 983 | 984 | test('limit number of keys in typed array', (assert) => { 985 | const serialize = stringify.configure({ 986 | maximumBreadth: 3 987 | }) 988 | const MAX = 100 989 | const arr = new Int32Array(MAX) 990 | 991 | for (let i = 0; i < MAX; i++) { 992 | arr[i] = i 993 | } 994 | // @ts-expect-error we want to explicitly test this behavior. 995 | arr.foobar = true 996 | const res = serialize(arr) 997 | const expected = '{"0":0,"1":1,"2":2,"...":"98 items not stringified"}' 998 | assert.equal(res, expected) 999 | const res2 = serialize(arr, (a, b) => b) 1000 | assert.equal(res2, expected) 1001 | const res3 = serialize(arr, [0, 1, 2]) 1002 | assert.equal(res3, '{"0":0,"1":1,"2":2}') 1003 | const res4 = serialize(arr, null, 4) 1004 | assert.equal(res4, `{ 1005 | "0": 0, 1006 | "1": 1, 1007 | "2": 2, 1008 | "...": "98 items not stringified" 1009 | }`) 1010 | assert.end() 1011 | }) 1012 | 1013 | test('show skipped keys even non were serliazable', (assert) => { 1014 | const serialize = stringify.configure({ 1015 | maximumBreadth: 1 1016 | }) 1017 | 1018 | const input = { a: Symbol('ignored'), b: Symbol('ignored') } 1019 | 1020 | const actual1 = serialize(input) 1021 | let expected = '{"...":"1 item not stringified"}' 1022 | assert.equal(actual1, expected) 1023 | 1024 | const actual2 = serialize(input, (a, b) => b) 1025 | assert.equal(actual2, expected) 1026 | 1027 | const actual3 = serialize(input, null, 1) 1028 | expected = '{\n "...": "1 item not stringified"\n}' 1029 | assert.equal(actual3, expected) 1030 | 1031 | const actual4 = serialize(input, (a, b) => b, 1) 1032 | assert.equal(actual4, expected) 1033 | 1034 | const actual5 = serialize(input, ['a']) 1035 | expected = '{}' 1036 | assert.equal(actual5, expected) 1037 | 1038 | const actual6 = serialize(input, ['a', 'b', 'c']) 1039 | assert.equal(actual6, expected) 1040 | 1041 | assert.end() 1042 | }) 1043 | 1044 | test('array replacer entries are unique', (assert) => { 1045 | const input = { 0: 0, b: 1 } 1046 | 1047 | const replacer = ['b', {}, [], 0, 'b', '0'] 1048 | // @ts-expect-error 1049 | const actual = stringify(input, replacer) 1050 | // @ts-expect-error 1051 | const expected = JSON.stringify(input, replacer) 1052 | assert.equal(actual, expected) 1053 | 1054 | assert.end() 1055 | }) 1056 | 1057 | test('should throw when maximumBreadth receives malformed input', (assert) => { 1058 | assert.throws(() => { 1059 | stringify.configure({ 1060 | // @ts-expect-error 1061 | maximumBreadth: '3' 1062 | }) 1063 | }) 1064 | assert.throws(() => { 1065 | stringify.configure({ 1066 | maximumBreadth: 3.1 1067 | }) 1068 | }) 1069 | assert.throws(() => { 1070 | stringify.configure({ 1071 | maximumBreadth: 0 1072 | }) 1073 | }) 1074 | assert.end() 1075 | }) 1076 | 1077 | test('check that all single characters are identical to JSON.stringify', (assert) => { 1078 | for (let i = 0; i < 2 ** 16; i++) { 1079 | const string = String.fromCharCode(i) 1080 | const actual = stringify(string) 1081 | const expected = JSON.stringify(string) 1082 | assert.equal(actual, expected) 1083 | } 1084 | assert.end() 1085 | }) 1086 | 1087 | test('check for lone surrogate pairs', { skip: Number(process.version.slice(1, 3)) <= 11 }, (assert) => { 1088 | const edgeChar = String.fromCharCode(0xd799) 1089 | 1090 | for (let charCode = 0xD800; charCode < 0xDFFF; charCode++) { 1091 | const surrogate = String.fromCharCode(charCode) 1092 | 1093 | assert.equal( 1094 | stringify(surrogate), 1095 | `"\\u${charCode.toString(16)}"` 1096 | ) 1097 | assert.equal( 1098 | stringify(`${'a'.repeat(200)}${surrogate}`), 1099 | `"${'a'.repeat(200)}\\u${charCode.toString(16)}"` 1100 | ) 1101 | assert.equal( 1102 | stringify(`${surrogate}${'a'.repeat(200)}`), 1103 | `"\\u${charCode.toString(16)}${'a'.repeat(200)}"` 1104 | ) 1105 | if (charCode < 0xdc00) { 1106 | const highSurrogate = surrogate 1107 | const lowSurrogate = String.fromCharCode(charCode + 1024) 1108 | assert.notOk( 1109 | stringify( 1110 | `${edgeChar}${highSurrogate}${lowSurrogate}${edgeChar}` 1111 | ).includes('\\u') 1112 | ) 1113 | assert.equal( 1114 | (stringify( 1115 | `${highSurrogate}${highSurrogate}${lowSurrogate}` 1116 | ).match(/\\u/g) || []).length, 1117 | 1 1118 | ) 1119 | } else { 1120 | assert.equal( 1121 | stringify(`${edgeChar}${surrogate}${edgeChar}`), 1122 | `"${edgeChar}\\u${charCode.toString(16)}${edgeChar}"` 1123 | ) 1124 | } 1125 | } 1126 | assert.end() 1127 | }) 1128 | 1129 | test('strict option possibilities', (assert) => { 1130 | assert.throws(() => { 1131 | // @ts-expect-error 1132 | stringify.configure({ strict: 1 }) 1133 | }, { 1134 | message: 'The "strict" argument must be of type boolean', 1135 | name: 'TypeError' 1136 | }) 1137 | 1138 | const serializer = stringify.configure({ strict: false }) 1139 | 1140 | serializer(NaN) 1141 | 1142 | const strictWithoutBigInt = stringify.configure({ strict: true, bigint: true }) 1143 | strictWithoutBigInt(5n) 1144 | 1145 | assert.throws(() => { 1146 | strictWithoutBigInt(NaN) 1147 | }, { 1148 | message: 'Object can not safely be stringified. Received type number (NaN)' 1149 | }) 1150 | 1151 | const strictWithoutCircular = stringify.configure({ strict: true, circularValue: 'Circular' }) 1152 | strictWithoutBigInt(5n) 1153 | 1154 | const circular = {} 1155 | circular.circular = circular 1156 | strictWithoutCircular(circular) 1157 | 1158 | assert.end() 1159 | }) 1160 | 1161 | test('strict option simple', (assert) => { 1162 | const strictSerializer = stringify.configure({ strict: true }) 1163 | 1164 | assert.throws(() => { 1165 | strictSerializer({ a: NaN }) 1166 | }, { 1167 | message: 'Object can not safely be stringified. Received type number (NaN)', 1168 | name: 'Error' 1169 | }) 1170 | 1171 | assert.throws(() => { 1172 | strictSerializer({ a: 5n }) 1173 | }, { 1174 | message: 'Object can not safely be stringified. Received type bigint (5)', 1175 | name: 'Error' 1176 | }) 1177 | 1178 | assert.throws(() => { 1179 | strictSerializer({ a () {} }) 1180 | }, { 1181 | message: 'Object can not safely be stringified. Received type function', 1182 | name: 'Error' 1183 | }) 1184 | 1185 | assert.throws(() => { 1186 | const circular = {} 1187 | circular.circular = circular 1188 | strictSerializer(circular) 1189 | }, { 1190 | message: 'Converting circular structure to JSON', 1191 | name: 'TypeError' 1192 | }) 1193 | 1194 | assert.end() 1195 | }) 1196 | 1197 | test('strict option indentation', (assert) => { 1198 | const strictSerializer = stringify.configure({ strict: true }) 1199 | 1200 | assert.throws(() => { 1201 | strictSerializer({ a: -Infinity }, null, 2) 1202 | }, { 1203 | message: 'Object can not safely be stringified. Received type number (-Infinity)', 1204 | name: 'Error' 1205 | }) 1206 | 1207 | assert.throws(() => { 1208 | strictSerializer({ a: 5n }, null, 2) 1209 | }, { 1210 | message: 'Object can not safely be stringified. Received type bigint (5)', 1211 | name: 'Error' 1212 | }) 1213 | 1214 | assert.throws(() => { 1215 | strictSerializer({ a () {} }, null, 2) 1216 | }, { 1217 | message: 'Object can not safely be stringified. Received type function', 1218 | name: 'Error' 1219 | }) 1220 | 1221 | assert.throws(() => { 1222 | const circular = {} 1223 | circular.circular = circular 1224 | strictSerializer(circular, null, 2) 1225 | }, { 1226 | message: 'Converting circular structure to JSON', 1227 | name: 'TypeError' 1228 | }) 1229 | 1230 | assert.end() 1231 | }) 1232 | 1233 | test('strict option replacer function', (assert) => { 1234 | const strictSerializer = stringify.configure({ strict: true }) 1235 | 1236 | assert.throws(() => { 1237 | strictSerializer(Symbol('test'), (_key_, value) => value) 1238 | }, { 1239 | message: 'Object can not safely be stringified. Received type symbol (Symbol(test))' 1240 | }) 1241 | 1242 | assert.throws(() => { 1243 | strictSerializer(5n, (_key_, value) => value) 1244 | }, { 1245 | message: 'Object can not safely be stringified. Received type bigint (5)' 1246 | }) 1247 | 1248 | assert.throws(() => { 1249 | strictSerializer(NaN, (_key_, value) => value) 1250 | }, { 1251 | message: 'Object can not safely be stringified. Received type number (NaN)' 1252 | }) 1253 | 1254 | assert.throws(() => { 1255 | const circular = {} 1256 | circular.circular = circular 1257 | strictSerializer(circular, (_key_, value) => value) 1258 | }, { 1259 | message: 'Converting circular structure to JSON', 1260 | name: 'TypeError' 1261 | }) 1262 | 1263 | assert.end() 1264 | }) 1265 | 1266 | test('strict option replacer array', (assert) => { 1267 | assert.throws(() => { 1268 | // @ts-expect-error 1269 | stringify.configure({ strict: 1 }) 1270 | }, { 1271 | message: 'The "strict" argument must be of type boolean', 1272 | name: 'Error' 1273 | }) 1274 | 1275 | const strictSerializer = stringify.configure({ strict: true }) 1276 | 1277 | assert.throws(() => { 1278 | strictSerializer({ a () {} }, ['a']) 1279 | }, { 1280 | message: 'Object can not safely be stringified. Received type function' 1281 | }) 1282 | 1283 | assert.throws(() => { 1284 | strictSerializer({ a: 5n }, ['a']) 1285 | }, { 1286 | message: 'Object can not safely be stringified. Received type bigint (5)' 1287 | }) 1288 | 1289 | assert.throws(() => { 1290 | strictSerializer({ a: Infinity }, ['a']) 1291 | }, { 1292 | message: 'Object can not safely be stringified. Received type number (Infinity)' 1293 | }) 1294 | 1295 | assert.throws(() => { 1296 | const circular = {} 1297 | circular.circular = circular 1298 | strictSerializer(circular, ['circular']) 1299 | }, { 1300 | message: 'Converting circular structure to JSON', 1301 | name: 'TypeError' 1302 | }) 1303 | 1304 | assert.end() 1305 | }) 1306 | 1307 | test('deterministic option possibilities', (assert) => { 1308 | assert.throws(() => { 1309 | // @ts-expect-error 1310 | stringify.configure({ deterministic: 1 }) 1311 | }, { 1312 | message: 'The "deterministic" argument must be of type boolean or comparator function', 1313 | name: 'TypeError' 1314 | }) 1315 | 1316 | const serializer1 = stringify.configure({ deterministic: false }) 1317 | serializer1(NaN) 1318 | 1319 | const serializer2 = stringify.configure({ deterministic: (a, b) => a.localeCompare(b) }) 1320 | serializer2(NaN) 1321 | 1322 | assert.end() 1323 | }) 1324 | 1325 | test('deterministic default sorting', function (assert) { 1326 | const serializer = stringify.configure({ deterministic: true }) 1327 | 1328 | const obj = { b: 2, c: 3, a: 1 } 1329 | const expected = '{\n "a": 1,\n "b": 2,\n "c": 3\n}' 1330 | const actual = serializer(obj, null, 1) 1331 | assert.equal(actual, expected) 1332 | 1333 | assert.end() 1334 | }) 1335 | 1336 | test('deterministic custom sorting', function (assert) { 1337 | // Descending 1338 | const serializer = stringify.configure({ deterministic: (a, b) => b.localeCompare(a) }) 1339 | 1340 | const obj = { b: 2, c: 3, a: 1 } 1341 | const expected = '{\n "c": 3,\n "b": 2,\n "a": 1\n}' 1342 | const actual = serializer(obj, null, 1) 1343 | assert.equal(actual, expected) 1344 | 1345 | assert.end() 1346 | }) 1347 | 1348 | test('safe mode safeguards against failing getters', function (assert) { 1349 | const serializer = stringify.configure({ safe: true }) 1350 | 1351 | const obj = { b: 2, c: { a: true, get b () { throw new Error('Oops') } }, a: 1 } 1352 | const expected = '{\n "a": 1,\n "b": 2,\n "c": "Error: Stringification failed. Message: Oops"\n}' 1353 | const actual = serializer(obj, null, 1) 1354 | assert.equal(actual, expected) 1355 | 1356 | assert.end() 1357 | }) 1358 | 1359 | test('safe mode safeguards against failing toJSON method', function (assert) { 1360 | const serializer = stringify.configure({ safe: true }) 1361 | 1362 | // eslint-disable-next-line 1363 | const obj = { b: 2, c: { a: true, toJSON () { throw 'Oops' } }, a: 1 } 1364 | const expected = '{\n "a": 1,\n "b": 2,\n "c": "Error: Stringification failed. Message: Oops"\n}' 1365 | const actual = serializer(obj, null, 1) 1366 | assert.equal(actual, expected) 1367 | 1368 | assert.end() 1369 | }) 1370 | 1371 | test('safe mode safeguards against failing getters and a difficult to stringify error', function (assert) { 1372 | const serializer = stringify.configure({ safe: true }) 1373 | 1374 | // eslint-disable-next-line 1375 | const obj = { b: 2, c: { a: true, toJSON () { throw { toString() { throw new Error('Yikes') } } } }, a: 1 } 1376 | const expected = '{\n "a": 1,\n "b": 2,\n "c": "Error: Stringification failed. Message: Failed"\n}' 1377 | const actual = serializer(obj, null, 1) 1378 | assert.equal(actual, expected) 1379 | 1380 | assert.end() 1381 | }) 1382 | -------------------------------------------------------------------------------- /test.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "5a660b85cc23d42973bb8f61", 4 | "index": 0, 5 | "guid": "cbd5519b-24d2-479b-bd07-c6d676d350e7", 6 | "isActive": true, 7 | "balance": "$3,182.51", 8 | "picture": "http://placehold.it/32x32", 9 | "age": 37, 10 | "eyeColor": "brown", 11 | "name": "Tami Burton", 12 | "gender": "female", 13 | "company": "PUSHCART", 14 | "email": "tamiburton@pushcart.com", 15 | "phone": "+1 (821) 429-2754", 16 | "address": "272 Nichols Avenue, Brownlee, Iowa, 8202", 17 | "about": "Incididunt aute irure ex dolore amet id dolor occaecat eu ullamco. Consectetur adipisicing ea Lorem laborum. Incididunt culpa nulla deserunt sit laboris pariatur occaecat est exercitation sit. Laboris non proident nulla do enim cillum laboris labore id est nisi commodo sint esse. Sit eiusmod ad elit consequat irure Lorem ex do id minim.\r\n", 18 | "registered": "2015-04-02T06:41:59 -02:00", 19 | "latitude": -49.91047, 20 | "longitude": 125.004934, 21 | "tags": [ 22 | "do", 23 | "sint", 24 | "tempor", 25 | "enim", 26 | "velit", 27 | "eu", 28 | "proident" 29 | ], 30 | "friends": [ 31 | { 32 | "id": 0, 33 | "name": "Montgomery Chandler" 34 | }, 35 | { 36 | "id": 1, 37 | "name": "Pollard Nixon" 38 | }, 39 | { 40 | "id": 2, 41 | "name": "Elvia Curry" 42 | } 43 | ], 44 | "greeting": "Hello, Tami Burton! You have 1 unread messages.", 45 | "favoriteFruit": "strawberry" 46 | }, 47 | { 48 | "_id": "5a660b857751ef05c308268b", 49 | "index": 1, 50 | "guid": "1628176c-c9f6-4756-b605-5d5815a8c05d", 51 | "isActive": false, 52 | "balance": "$2,649.96", 53 | "picture": "http://placehold.it/32x32", 54 | "age": 38, 55 | "eyeColor": "brown", 56 | "name": "Jami Buck", 57 | "gender": "female", 58 | "company": "BICOL", 59 | "email": "jamibuck@bicol.com", 60 | "phone": "+1 (866) 562-3777", 61 | "address": "141 Gallatin Place, Geyserville, Alabama, 7547", 62 | "about": "In occaecat veniam nisi do velit occaecat laboris cupidatat est voluptate. Labore quis id enim enim do tempor non ad dolor consectetur ea nisi. Eu cupidatat aute occaecat et in consequat aute nisi cupidatat est. Occaecat aliquip magna proident nostrud magna ad deserunt. Lorem Lorem ipsum irure laborum est Lorem mollit consequat eu. Et irure aliqua enim sit excepteur mollit.\r\n", 63 | "registered": "2015-08-21T04:06:37 -02:00", 64 | "latitude": -32.157219, 65 | "longitude": 167.849252, 66 | "tags": [ 67 | "aute", 68 | "laborum", 69 | "enim", 70 | "ut", 71 | "proident", 72 | "aliqua", 73 | "est" 74 | ], 75 | "friends": [ 76 | { 77 | "id": 0, 78 | "name": "Nell Foreman" 79 | }, 80 | { 81 | "id": 1, 82 | "name": "Dillard Waters" 83 | }, 84 | { 85 | "id": 2, 86 | "name": "Tabatha Hunter" 87 | } 88 | ], 89 | "greeting": "Hello, Jami Buck! You have 9 unread messages.", 90 | "favoriteFruit": "banana" 91 | }, 92 | { 93 | "_id": "5a660b8524acee2f78c68ad3", 94 | "index": 2, 95 | "guid": "e6b479c7-995f-4742-a939-f7c6b03a9d84", 96 | "isActive": false, 97 | "balance": "$1,199.92", 98 | "picture": "http://placehold.it/32x32", 99 | "age": 29, 100 | "eyeColor": "blue", 101 | "name": "Aisha Conner", 102 | "gender": "female", 103 | "company": "ZILPHUR", 104 | "email": "aishaconner@zilphur.com", 105 | "phone": "+1 (806) 419-3132", 106 | "address": "815 Lafayette Avenue, Knowlton, Connecticut, 4266", 107 | "about": "Culpa proident exercitation consequat amet do nisi qui dolore do exercitation ea. Officia esse est mollit cillum. Eu qui laboris minim sint pariatur. Esse occaecat est esse elit.\r\n", 108 | "registered": "2016-07-28T05:45:25 -02:00", 109 | "latitude": -25.704426, 110 | "longitude": -72.539193, 111 | "tags": [ 112 | "adipisicing", 113 | "exercitation", 114 | "sunt", 115 | "est", 116 | "ut", 117 | "do", 118 | "est" 119 | ], 120 | "friends": [ 121 | { 122 | "id": 0, 123 | "name": "Barbara Mckee" 124 | }, 125 | { 126 | "id": 1, 127 | "name": "Rachel Pennington" 128 | }, 129 | { 130 | "id": 2, 131 | "name": "Beverley Christian" 132 | } 133 | ], 134 | "greeting": "Hello, Aisha Conner! You have 5 unread messages.", 135 | "favoriteFruit": "strawberry" 136 | }, 137 | { 138 | "_id": "5a660b85d031eea36971a882", 139 | "index": 3, 140 | "guid": "d52d9dcc-db48-445b-9c5f-b435fda26099", 141 | "isActive": true, 142 | "balance": "$3,059.34", 143 | "picture": "http://placehold.it/32x32", 144 | "age": 22, 145 | "eyeColor": "green", 146 | "name": "Brooke Whitfield", 147 | "gender": "female", 148 | "company": "JIMBIES", 149 | "email": "brookewhitfield@jimbies.com", 150 | "phone": "+1 (991) 538-3370", 151 | "address": "906 Lincoln Terrace, Bascom, Utah, 2326", 152 | "about": "Lorem non sit mollit proident. Velit minim ex quis et amet proident officia ut elit ad est culpa sit mollit. Ullamco tempor adipisicing consectetur laborum esse minim laborum est exercitation nostrud eu. Fugiat sint consectetur velit reprehenderit. Ullamco pariatur mollit do proident anim quis tempor elit. In est ad sit sint ea laborum minim.\r\n", 153 | "registered": "2017-02-22T02:41:41 -01:00", 154 | "latitude": -33.567516, 155 | "longitude": -87.814848, 156 | "tags": [ 157 | "eu", 158 | "eu", 159 | "sit", 160 | "incididunt", 161 | "ea", 162 | "laborum", 163 | "ullamco" 164 | ], 165 | "friends": [ 166 | { 167 | "id": 0, 168 | "name": "Schwartz Tanner" 169 | }, 170 | { 171 | "id": 1, 172 | "name": "Gutierrez Porter" 173 | }, 174 | { 175 | "id": 2, 176 | "name": "Terra Mcguire" 177 | } 178 | ], 179 | "greeting": "Hello, Brooke Whitfield! You have 6 unread messages.", 180 | "favoriteFruit": "strawberry" 181 | }, 182 | { 183 | "_id": "5a660b858f41af2de9dfd5be", 184 | "index": 4, 185 | "guid": "88a4bdd5-e4fa-440d-8345-50c149a636f9", 186 | "isActive": true, 187 | "balance": "$1,867.97", 188 | "picture": "http://placehold.it/32x32", 189 | "age": 39, 190 | "eyeColor": "brown", 191 | "name": "Shauna Leonard", 192 | "gender": "female", 193 | "company": "XUMONK", 194 | "email": "shaunaleonard@xumonk.com", 195 | "phone": "+1 (890) 549-3263", 196 | "address": "865 Folsom Place, Gorham, Pennsylvania, 9359", 197 | "about": "Veniam ullamco elit do in velit tempor eiusmod eiusmod sit duis. Esse sit cupidatat culpa nisi mollit nisi ut nisi nulla nulla. Reprehenderit culpa anim ea dolore enim occaecat est. Incididunt incididunt dolor anim ullamco qui labore consequat exercitation elit sint incididunt culpa labore. Cupidatat proident duis tempor nostrud pariatur adipisicing. Tempor occaecat proident deserunt non irure irure. Lorem sint anim dolore exercitation cupidatat commodo proident labore irure commodo sunt.\r\n", 198 | "registered": "2016-12-17T08:40:42 -01:00", 199 | "latitude": -50.040955, 200 | "longitude": -39.104934, 201 | "tags": [ 202 | "magna", 203 | "et", 204 | "ex", 205 | "duis", 206 | "ex", 207 | "non", 208 | "mollit" 209 | ], 210 | "friends": [ 211 | { 212 | "id": 0, 213 | "name": "Berry Carr" 214 | }, 215 | { 216 | "id": 1, 217 | "name": "Ashley Williams" 218 | }, 219 | { 220 | "id": 2, 221 | "name": "Hickman Pace" 222 | } 223 | ], 224 | "greeting": "Hello, Shauna Leonard! You have 9 unread messages.", 225 | "favoriteFruit": "banana" 226 | } 227 | ] -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "allowJs": true, 5 | "checkJs": true, 6 | "sourceMap": false, 7 | "declaration": false, 8 | "noEmit": true, 9 | "downlevelIteration": false, 10 | "experimentalDecorators": false, 11 | "moduleResolution": "nodenext", 12 | "importHelpers": false, 13 | "target": "esnext", 14 | "module": "CommonJS", 15 | "strict": true, 16 | "noImplicitAny": false, 17 | "lib": [ 18 | "esnext", 19 | "dom" 20 | ] 21 | }, 22 | "include": ["**/*.js", "**/*.ts"], 23 | "exclude": ["compare.js", "benchmark.js", "./coverage"] 24 | } 25 | --------------------------------------------------------------------------------