├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md ├── test.js ├── test.js.md └── test.js.snap /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 20 14 | - 18 15 | - 16 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type Options = { 2 | /** 3 | The indentation of the JSON. 4 | 5 | By default, the JSON is not indented. 6 | 7 | Set it to `'\t'` for tab indentation or the number of spaces you want. 8 | */ 9 | readonly indentation?: string | number; 10 | }; 11 | 12 | /** 13 | Serialize objects to JSON with handling for circular references. 14 | 15 | @example 16 | ``` 17 | import safeStringify from 'safe-stringify'; 18 | 19 | const foo = {a: true}; 20 | foo.b = foo; 21 | 22 | console.log(safeStringify(foo)); 23 | //=> '{"a":true,"b":"[Circular]"}' 24 | 25 | console.log(JSON.stringify(foo)); 26 | //=> TypeError: Converting circular structure to JSON 27 | ``` 28 | */ 29 | export default function safeStringify(value: unknown, options?: Options): string; 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function safeStringifyReplacer(seen) { 2 | const replacer = function (key, value) { 3 | // Handle objects with a custom `.toJSON()` method. 4 | if (typeof value?.toJSON === 'function') { 5 | value = value.toJSON(); 6 | } 7 | 8 | if (!(value !== null && typeof value === 'object')) { 9 | return value; 10 | } 11 | 12 | if (seen.has(value)) { 13 | return '[Circular]'; 14 | } 15 | 16 | seen.add(value); 17 | 18 | const newValue = Array.isArray(value) ? [] : {}; 19 | 20 | for (const [key2, value2] of Object.entries(value)) { 21 | newValue[key2] = replacer(key2, value2); 22 | } 23 | 24 | seen.delete(value); 25 | 26 | return newValue; 27 | }; 28 | 29 | return replacer; 30 | } 31 | 32 | export default function safeStringify(object, {indentation} = {}) { 33 | const seen = new WeakSet(); 34 | return JSON.stringify(object, safeStringifyReplacer(seen), indentation); 35 | } 36 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import safeStringify from './index.js'; 3 | 4 | expectType(safeStringify(1)); 5 | safeStringify('foo', {indentation: '\t'}); 6 | safeStringify('foo', {indentation: 2}); 7 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safe-stringify", 3 | "version": "1.2.0", 4 | "description": "Serialize objects to JSON with handling for circular references", 5 | "license": "MIT", 6 | "repository": "sindresorhus/safe-stringify", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=16" 21 | }, 22 | "scripts": { 23 | "test": "xo && ava && tsd" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts" 28 | ], 29 | "keywords": [ 30 | "json", 31 | "stringify", 32 | "safe", 33 | "circular", 34 | "destroy", 35 | "serialize", 36 | "encode", 37 | "cyclic", 38 | "object" 39 | ], 40 | "devDependencies": { 41 | "ava": "^6.1.3", 42 | "tsd": "^0.31.1", 43 | "xo": "^0.58.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # safe-stringify 2 | 3 | > Serialize objects to JSON with handling for circular references 4 | 5 | `JSON.stringify()` throws an error if the object contains circular references. This package replaces circular references with `"[Circular]"`. 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install safe-stringify 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import safeStringify from 'safe-stringify'; 17 | 18 | const foo = {a: true}; 19 | foo.b = foo; 20 | 21 | console.log(safeStringify(foo)); 22 | //=> '{"a":true,"b":"[Circular]"}' 23 | 24 | console.log(JSON.stringify(foo)); 25 | //=> TypeError: Converting circular structure to JSON 26 | ``` 27 | 28 | ## API 29 | 30 | ### safeStringify(value, options?) 31 | 32 | Returns a string. 33 | 34 | *Note: There is no `replacer` option as I didn't need that, but “pull request welcome” if you need it.* 35 | 36 | #### value 37 | 38 | Type: `unknown` 39 | 40 | The value to convert to a JSON string. 41 | 42 | #### options 43 | 44 | Type: `object` 45 | 46 | ##### indentation 47 | 48 | Type: `'string' | 'number'` 49 | 50 | The indentation of the JSON. 51 | 52 | By default, the JSON is not indented. Set it to `'\t'` for tab indentation or the number of spaces you want. 53 | 54 | ## FAQ 55 | 56 | ### Why another safe stringify package? 57 | 58 | The existing ones either did too much, did it incorrectly, or used inefficient code (not using `WeakSet`). For example, many packages incorrectly replaced all duplicate objects, not just circular references, and did not handle circular arrays. 59 | 60 | ## Related 61 | 62 | - [decircular](https://github.com/sindresorhus/decircular) - Remove circular references from objects 63 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import safeStringify from './index.js'; 3 | 4 | const options = { 5 | indentation: '\t', 6 | }; 7 | 8 | test('main', t => { 9 | const fixture = { 10 | a: true, 11 | b: { 12 | c: 1, 13 | }, 14 | }; 15 | 16 | t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t')); 17 | }); 18 | 19 | test('circular object', t => { 20 | const fixture = { 21 | a: true, 22 | }; 23 | 24 | fixture.b = fixture; 25 | 26 | fixture.c = [fixture, fixture.b]; 27 | 28 | fixture.d = { 29 | e: fixture.c, 30 | }; 31 | 32 | t.snapshot(safeStringify(fixture, options)); 33 | }); 34 | 35 | test('circular object 2', t => { 36 | const fixture2 = { 37 | c: true, 38 | }; 39 | 40 | const fixture = { 41 | a: fixture2, 42 | b: fixture2, 43 | }; 44 | 45 | t.snapshot(safeStringify(fixture, options)); 46 | }); 47 | 48 | test('circular array', t => { 49 | const fixture = [1]; 50 | 51 | fixture.push(fixture, fixture); 52 | 53 | t.snapshot(safeStringify(fixture, options)); 54 | }); 55 | 56 | test('multiple circular objects in array', t => { 57 | const fixture = { 58 | a: true, 59 | }; 60 | 61 | fixture.b = fixture; 62 | 63 | t.snapshot(safeStringify([fixture, fixture], options)); 64 | }); 65 | 66 | test('multiple circular objects in object', t => { 67 | const fixture = { 68 | a: true, 69 | }; 70 | 71 | fixture.b = fixture; 72 | 73 | t.snapshot(safeStringify({x: fixture, y: fixture}, options)); 74 | }); 75 | 76 | test('nested non-circular object', t => { 77 | const fixture = { 78 | a: { 79 | b: { 80 | c: { 81 | d: 1, 82 | }, 83 | }, 84 | }, 85 | }; 86 | 87 | t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t')); 88 | }); 89 | 90 | test('nested circular object', t => { 91 | const fixture = { 92 | a: { 93 | b: { 94 | c: {}, 95 | }, 96 | }, 97 | }; 98 | 99 | fixture.a.b.c.d = fixture.a; 100 | 101 | t.snapshot(safeStringify(fixture, options)); 102 | }); 103 | 104 | test('complex object with circular and non-circular references', t => { 105 | const shared = {x: 1}; 106 | const circular = {y: 2}; 107 | circular.self = circular; 108 | 109 | const fixture = { 110 | a: shared, 111 | b: { 112 | c: shared, 113 | d: circular, 114 | }, 115 | e: circular, 116 | }; 117 | 118 | t.snapshot(safeStringify(fixture, options)); 119 | }); 120 | 121 | test('object with circular references at different depths', t => { 122 | const fixture = { 123 | a: { 124 | b: { 125 | c: {}, 126 | }, 127 | }, 128 | }; 129 | 130 | fixture.a.b.c.d = fixture.a; 131 | fixture.a.b.c.e = fixture.a.b; 132 | 133 | t.snapshot(safeStringify(fixture, options)); 134 | }); 135 | 136 | test('object with value as a circular reference', t => { 137 | const fixture = { 138 | a: 1, 139 | b: 2, 140 | }; 141 | 142 | fixture.self = fixture; 143 | 144 | t.snapshot(safeStringify(fixture, options)); 145 | }); 146 | 147 | test('empty object', t => { 148 | const fixture = {}; 149 | 150 | t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t')); 151 | }); 152 | 153 | test('object with null value', t => { 154 | const fixture = { 155 | a: null, 156 | }; 157 | 158 | t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t')); 159 | }); 160 | 161 | test('object with undefined value', t => { 162 | const fixture = { 163 | a: undefined, 164 | }; 165 | 166 | t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t')); 167 | }); 168 | 169 | test('circular object with multiple nested circular references', t => { 170 | const fixture = { 171 | a: { 172 | b: { 173 | c: {}, 174 | }, 175 | }, 176 | }; 177 | 178 | fixture.a.b.c.d = fixture.a; 179 | fixture.a.b.c.e = fixture.a.b; 180 | fixture.a.b.c.f = fixture.a.b.c; 181 | 182 | t.snapshot(safeStringify(fixture, options)); 183 | }); 184 | 185 | test('circular array with nested circular arrays', t => { 186 | const fixture = [[1, 2, 3]]; 187 | 188 | fixture.push(fixture, [fixture, fixture]); 189 | 190 | t.snapshot(safeStringify(fixture, options)); 191 | }); 192 | 193 | test('object with circular reference to parent and grandparent', t => { 194 | const fixture = { 195 | a: { 196 | b: { 197 | c: {}, 198 | }, 199 | }, 200 | }; 201 | 202 | fixture.a.b.c.parent = fixture.a.b; 203 | fixture.a.b.c.grandparent = fixture.a; 204 | 205 | t.snapshot(safeStringify(fixture, options)); 206 | }); 207 | 208 | test('array containing objects with the same circular reference', t => { 209 | const circular = {a: 1}; 210 | circular.self = circular; 211 | 212 | const fixture = [ 213 | {b: 2, c: circular}, 214 | {d: 3, e: circular}, 215 | ]; 216 | 217 | t.snapshot(safeStringify(fixture, options)); 218 | }); 219 | 220 | test('Date object', t => { 221 | const fixture = { 222 | date: new Date('2024-06-12T16:06:46.442Z'), 223 | }; 224 | 225 | t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t')); 226 | }); 227 | 228 | test('object with toJSON method', t => { 229 | const fixture = { 230 | a: 1, 231 | toJSON() { 232 | return {b: 2}; 233 | }, 234 | }; 235 | 236 | t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t')); 237 | }); 238 | 239 | test('complex object with Date and toJSON', t => { 240 | const fixture = { 241 | date: new Date('2024-06-12T16:06:46.442Z'), 242 | nested: { 243 | toJSON() { 244 | return {b: 2}; 245 | }, 246 | }, 247 | }; 248 | 249 | t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t')); 250 | }); 251 | 252 | test('circular object with Date', t => { 253 | const fixture = { 254 | date: new Date('2024-06-12T16:06:46.442Z'), 255 | }; 256 | 257 | fixture.self = fixture; 258 | 259 | const expected = JSON.stringify({date: '2024-06-12T16:06:46.442Z', self: '[Circular]'}, undefined, '\t'); 260 | t.is(safeStringify(fixture, options), expected); 261 | }); 262 | 263 | test('nested toJSON methods', t => { 264 | const fixture = { 265 | a: { 266 | toJSON() { 267 | return {b: 2}; 268 | }, 269 | }, 270 | b: { 271 | toJSON() { 272 | return {c: 3}; 273 | }, 274 | }, 275 | }; 276 | 277 | t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t')); 278 | }); 279 | 280 | test('toJSON method returning circular object', t => { 281 | const fixture = { 282 | a: 1, 283 | toJSON() { 284 | const x = {b: 2}; 285 | x.self = x; 286 | return x; 287 | }, 288 | }; 289 | 290 | const expected = JSON.stringify({b: 2, self: '[Circular]'}, undefined, '\t'); 291 | t.is(safeStringify(fixture, options), expected); 292 | }); 293 | 294 | test('array with objects having toJSON methods', t => { 295 | const fixture = [ 296 | { 297 | toJSON() { 298 | return {a: 1}; 299 | }, 300 | }, 301 | { 302 | toJSON() { 303 | return {b: 2}; 304 | }, 305 | }, 306 | ]; 307 | 308 | t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t')); 309 | }); 310 | 311 | test('array with Date objects and toJSON methods', t => { 312 | const fixture = [ 313 | new Date('2024-06-12T16:06:46.442Z'), 314 | { 315 | toJSON() { 316 | return {b: 2}; 317 | }, 318 | }, 319 | ]; 320 | 321 | t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t')); 322 | }); 323 | 324 | test('complex object with circular references and toJSON', t => { 325 | const shared = { 326 | x: 1, 327 | toJSON() { 328 | return { 329 | x: this.x, 330 | }; 331 | }, 332 | }; 333 | 334 | const circular = { 335 | y: 2, 336 | toJSON() { 337 | return { 338 | y: this.y, 339 | self: '[Circular]', 340 | }; 341 | }, 342 | }; 343 | 344 | circular.self = circular; 345 | 346 | const fixture = { 347 | a: shared, 348 | b: { 349 | c: shared, 350 | d: circular, 351 | }, 352 | e: circular, 353 | }; 354 | 355 | const expected = JSON.stringify({ 356 | a: { 357 | x: 1, 358 | }, 359 | b: { 360 | c: { 361 | x: 1, 362 | }, 363 | d: { 364 | y: 2, 365 | self: '[Circular]', 366 | }, 367 | }, 368 | e: { 369 | y: 2, 370 | self: '[Circular]', 371 | }, 372 | }, undefined, '\t'); 373 | 374 | t.is(safeStringify(fixture, options), expected); 375 | }); 376 | -------------------------------------------------------------------------------- /test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test.js` 2 | 3 | The actual snapshot is saved in `test.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## circular object 8 | 9 | > Snapshot 1 10 | 11 | `{␊ 12 | "a": true,␊ 13 | "b": "[Circular]",␊ 14 | "c": [␊ 15 | "[Circular]",␊ 16 | "[Circular]"␊ 17 | ],␊ 18 | "d": {␊ 19 | "e": [␊ 20 | "[Circular]",␊ 21 | "[Circular]"␊ 22 | ]␊ 23 | }␊ 24 | }` 25 | 26 | ## circular object 2 27 | 28 | > Snapshot 1 29 | 30 | `{␊ 31 | "a": {␊ 32 | "c": true␊ 33 | },␊ 34 | "b": {␊ 35 | "c": true␊ 36 | }␊ 37 | }` 38 | 39 | ## circular array 40 | 41 | > Snapshot 1 42 | 43 | `[␊ 44 | 1,␊ 45 | "[Circular]",␊ 46 | "[Circular]"␊ 47 | ]` 48 | 49 | ## multiple circular objects in array 50 | 51 | > Snapshot 1 52 | 53 | `[␊ 54 | {␊ 55 | "a": true,␊ 56 | "b": "[Circular]"␊ 57 | },␊ 58 | {␊ 59 | "a": true,␊ 60 | "b": "[Circular]"␊ 61 | }␊ 62 | ]` 63 | 64 | ## multiple circular objects in object 65 | 66 | > Snapshot 1 67 | 68 | `{␊ 69 | "x": {␊ 70 | "a": true,␊ 71 | "b": "[Circular]"␊ 72 | },␊ 73 | "y": {␊ 74 | "a": true,␊ 75 | "b": "[Circular]"␊ 76 | }␊ 77 | }` 78 | 79 | ## nested circular object 80 | 81 | > Snapshot 1 82 | 83 | `{␊ 84 | "a": {␊ 85 | "b": {␊ 86 | "c": {␊ 87 | "d": "[Circular]"␊ 88 | }␊ 89 | }␊ 90 | }␊ 91 | }` 92 | 93 | ## complex object with circular and non-circular references 94 | 95 | > Snapshot 1 96 | 97 | `{␊ 98 | "a": {␊ 99 | "x": 1␊ 100 | },␊ 101 | "b": {␊ 102 | "c": {␊ 103 | "x": 1␊ 104 | },␊ 105 | "d": {␊ 106 | "y": 2,␊ 107 | "self": "[Circular]"␊ 108 | }␊ 109 | },␊ 110 | "e": {␊ 111 | "y": 2,␊ 112 | "self": "[Circular]"␊ 113 | }␊ 114 | }` 115 | 116 | ## object with circular references at different depths 117 | 118 | > Snapshot 1 119 | 120 | `{␊ 121 | "a": {␊ 122 | "b": {␊ 123 | "c": {␊ 124 | "d": "[Circular]",␊ 125 | "e": "[Circular]"␊ 126 | }␊ 127 | }␊ 128 | }␊ 129 | }` 130 | 131 | ## object with value as a circular reference 132 | 133 | > Snapshot 1 134 | 135 | `{␊ 136 | "a": 1,␊ 137 | "b": 2,␊ 138 | "self": "[Circular]"␊ 139 | }` 140 | 141 | ## circular object with multiple nested circular references 142 | 143 | > Snapshot 1 144 | 145 | `{␊ 146 | "a": {␊ 147 | "b": {␊ 148 | "c": {␊ 149 | "d": "[Circular]",␊ 150 | "e": "[Circular]",␊ 151 | "f": "[Circular]"␊ 152 | }␊ 153 | }␊ 154 | }␊ 155 | }` 156 | 157 | ## circular array with nested circular arrays 158 | 159 | > Snapshot 1 160 | 161 | `[␊ 162 | [␊ 163 | 1,␊ 164 | 2,␊ 165 | 3␊ 166 | ],␊ 167 | "[Circular]",␊ 168 | [␊ 169 | "[Circular]",␊ 170 | "[Circular]"␊ 171 | ]␊ 172 | ]` 173 | 174 | ## object with circular reference to parent and grandparent 175 | 176 | > Snapshot 1 177 | 178 | `{␊ 179 | "a": {␊ 180 | "b": {␊ 181 | "c": {␊ 182 | "parent": "[Circular]",␊ 183 | "grandparent": "[Circular]"␊ 184 | }␊ 185 | }␊ 186 | }␊ 187 | }` 188 | 189 | ## array containing objects with the same circular reference 190 | 191 | > Snapshot 1 192 | 193 | `[␊ 194 | {␊ 195 | "b": 2,␊ 196 | "c": {␊ 197 | "a": 1,␊ 198 | "self": "[Circular]"␊ 199 | }␊ 200 | },␊ 201 | {␊ 202 | "d": 3,␊ 203 | "e": {␊ 204 | "a": 1,␊ 205 | "self": "[Circular]"␊ 206 | }␊ 207 | }␊ 208 | ]` 209 | -------------------------------------------------------------------------------- /test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/safe-stringify/d0512ee503d6109ec6d48f145be51eeff86c7ee5/test.js.snap --------------------------------------------------------------------------------