├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── license ├── package.json ├── readme.md └── test.js /.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 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Remove circular references from objects. 3 | 4 | @returns A deep copy of the given object or array with circular references removed. 5 | 6 | @example 7 | ``` 8 | import decircular from 'decircular'; 9 | 10 | const object = { 11 | a: 1, 12 | b: { 13 | c: 2 14 | } 15 | }; 16 | 17 | object.b.d = object.b; // Creates a circular reference 18 | 19 | console.log(decircular(object)); 20 | // { 21 | // a: 1, 22 | // b: { 23 | // c: 2, 24 | // d: '[Circular *b]' 25 | // } 26 | // } 27 | ``` 28 | */ 29 | export default function decircular(object: T): T; // eslint-disable-line @typescript-eslint/ban-types 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export default function decircular(object) { 2 | const seenObjects = new WeakMap(); 3 | 4 | function internalDecircular(value, path = []) { 5 | if (!(value !== null && typeof value === 'object')) { 6 | return value; 7 | } 8 | 9 | const existingPath = seenObjects.get(value); 10 | if (existingPath) { 11 | return `[Circular *${existingPath.join('.')}]`; 12 | } 13 | 14 | seenObjects.set(value, path); 15 | 16 | const newValue = Array.isArray(value) ? [] : {}; 17 | 18 | for (const [key2, value2] of Object.entries(value)) { 19 | newValue[key2] = internalDecircular(value2, [...path, key2]); 20 | } 21 | 22 | seenObjects.delete(value); 23 | 24 | return newValue; 25 | } 26 | 27 | return internalDecircular(object); 28 | } 29 | -------------------------------------------------------------------------------- /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": "decircular", 3 | "version": "1.0.0", 4 | "description": "Remove circular references from objects", 5 | "license": "MIT", 6 | "repository": "sindresorhus/decircular", 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": ">=18" 21 | }, 22 | "scripts": { 23 | "test": "xo && ava && tsc index.d.ts" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts" 28 | ], 29 | "keywords": [ 30 | "circular", 31 | "object", 32 | "destroy", 33 | "remove", 34 | "reference", 35 | "ref", 36 | "copy", 37 | "array", 38 | "serialization", 39 | "json", 40 | "stringify", 41 | "cyclic" 42 | ], 43 | "devDependencies": { 44 | "ava": "^5.3.1", 45 | "typescript": "^5.2.2", 46 | "xo": "^0.56.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # decircular 2 | 3 | > Remove circular references from objects 4 | 5 | Circular references occur in JavaScript when an object references itself or creates a loop of references involving other objects. This can lead to issues like infinite loops and errors during serialization (e.g., with `JSON.stringify`). This package replaces circular references in objects or arrays with clear path notations (e.g., `[Circular *a.1.b]`). Ideal for data serialization, debugging, and logging. 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install decircular 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import decircular from 'decircular'; 17 | 18 | const object = { 19 | a: 1, 20 | b: { 21 | c: 2 22 | } 23 | }; 24 | 25 | object.b.d = object.b; // Creates a circular reference 26 | 27 | console.log(decircular(object)); 28 | /* 29 | { 30 | a: 1, 31 | b: { 32 | c: 2, 33 | d: '[Circular *b]' 34 | } 35 | } 36 | */ 37 | ``` 38 | 39 | ## API 40 | 41 | ### decircular(object) 42 | 43 | Returns a deep copy of the given object or array with circular references removed. 44 | 45 | ## Related 46 | 47 | - [safe-stringify](https://github.com/sindresorhus/safe-stringify) - Serialize objects to JSON with handling for circular references 48 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import decircular from './index.js'; 3 | 4 | test('handles circular references in objects', t => { 5 | const object = {a: 1}; 6 | object.b = object; // Circular reference 7 | 8 | t.deepEqual(decircular(object), { 9 | a: 1, 10 | b: '[Circular *]', 11 | }); 12 | }); 13 | 14 | test('handles circular references in nested objects', t => { 15 | const object = { 16 | a: 1, 17 | c: {}, 18 | }; 19 | 20 | object.c.d = object.c; 21 | 22 | t.deepEqual(decircular(object), { 23 | a: 1, 24 | c: { 25 | d: '[Circular *c]', 26 | }, 27 | }); 28 | }); 29 | 30 | test('handles circular references in arrays', t => { 31 | const array = [1, 2, 3]; 32 | array.push(array); // Circular reference 33 | 34 | t.deepEqual(decircular(array), [1, 2, 3, '[Circular *]']); 35 | }); 36 | 37 | test('handles non-circular references in objects', t => { 38 | const object = { 39 | a: { 40 | b: { 41 | c: { 42 | d: 1, 43 | }, 44 | }, 45 | }, 46 | }; 47 | object.a.b.e = object.a.b.c; 48 | 49 | t.deepEqual(decircular(object), { 50 | a: { 51 | b: { 52 | c: { 53 | d: 1, 54 | }, 55 | e: { 56 | d: 1, 57 | }, 58 | }, 59 | }, 60 | }); 61 | }); 62 | 63 | test('handles complex structures with multiple circular references', t => { 64 | const object = { 65 | a: 1, 66 | b: { 67 | c: 2, 68 | }, 69 | d: [], 70 | }; 71 | 72 | object.b.e = object; 73 | object.d.push(object.b); 74 | 75 | t.deepEqual(decircular(object), { 76 | a: 1, 77 | b: { 78 | c: 2, 79 | e: '[Circular *]', 80 | }, 81 | d: [ 82 | { 83 | c: 2, 84 | e: '[Circular *]', 85 | }, 86 | ], 87 | }); 88 | }); 89 | 90 | test('handles circular references with array indices in the path', t => { 91 | const object = { 92 | a: [ 93 | { 94 | b: 1, 95 | c: 1, 96 | }, 97 | ], 98 | }; 99 | 100 | object.a[0].d = object.a[0]; // Circular reference to the array element at index 1 101 | 102 | t.deepEqual(decircular(object), { 103 | a: [ 104 | { 105 | b: 1, 106 | c: 1, 107 | d: '[Circular *a.0]', 108 | }, 109 | ], 110 | }); 111 | }); 112 | 113 | test('handles deep nested circular references', t => { 114 | const object = { 115 | level1: { 116 | level2: { 117 | level3: {}, 118 | }, 119 | }, 120 | }; 121 | 122 | object.level1.level2.level3 = object.level1; // Circular reference to a higher level 123 | 124 | t.deepEqual(decircular(object), { 125 | level1: { 126 | level2: { 127 | level3: '[Circular *level1]', 128 | }, 129 | }, 130 | }); 131 | }); 132 | 133 | test('handles circular references in objects within arrays', t => { 134 | const object = { 135 | a: [], 136 | b: [], 137 | }; 138 | 139 | object.a[0] = object.b; // Circular reference to another object in the array 140 | object.b[0] = object.a; // Circular reference to another object in the array 141 | 142 | t.deepEqual(decircular(object), { 143 | a: [['[Circular *a]']], 144 | b: [['[Circular *b]']], 145 | }); 146 | }); 147 | 148 | test('handles self-referencing arrays', t => { 149 | const array = []; 150 | array[0] = array; // Array references itself 151 | 152 | t.deepEqual(decircular(array), ['[Circular *]']); 153 | }); 154 | 155 | test('handles circular references in mixed arrays and objects', t => { 156 | const object = { 157 | a: 1, 158 | b: [ 159 | 2, 160 | { 161 | c: 3, 162 | }, 163 | ], 164 | }; 165 | 166 | object.b[1].ref = object; // Circular reference from an object in an array to the root 167 | 168 | t.deepEqual(decircular(object), { 169 | a: 1, 170 | b: [ 171 | 2, 172 | { 173 | c: 3, 174 | ref: '[Circular *]', 175 | }, 176 | ], 177 | }); 178 | }); 179 | 180 | test('handles circular references involving arrays and objects', t => { 181 | const object = { 182 | a: [ 183 | { 184 | b: 1, 185 | }, 186 | ], 187 | }; 188 | 189 | object.a.push(object); // Circular reference from array to object 190 | 191 | t.deepEqual(decircular(object), { 192 | a: [ 193 | { 194 | b: 1, 195 | }, 196 | '[Circular *]', 197 | ], 198 | }); 199 | }); 200 | 201 | test('handles empty objects and arrays', t => { 202 | const object = {}; 203 | const array = []; 204 | object.ref = object; // Circular reference in an empty object 205 | array.push(array); // Circular reference in an empty array 206 | 207 | t.deepEqual(decircular(object), {ref: '[Circular *]'}); 208 | t.deepEqual(decircular(array), ['[Circular *]']); 209 | }); 210 | 211 | test('handles circular references in large complex objects', t => { 212 | const complexObject = { 213 | a: { 214 | b: { 215 | c: { 216 | d: 1, 217 | }, 218 | }, 219 | }, 220 | e: { 221 | f: { 222 | g: 2, 223 | }, 224 | }, 225 | }; 226 | 227 | complexObject.a.b.c.ref = complexObject.e.f; 228 | complexObject.e.f.ref = complexObject.a.b; 229 | 230 | t.deepEqual(decircular(complexObject), { 231 | a: { 232 | b: { 233 | c: { 234 | d: 1, 235 | ref: { 236 | g: 2, 237 | ref: '[Circular *a.b]', 238 | }, 239 | }, 240 | }, 241 | }, 242 | e: { 243 | f: { 244 | g: 2, 245 | ref: { 246 | c: { 247 | d: 1, 248 | ref: '[Circular *e.f]', 249 | }, 250 | }, 251 | }, 252 | }, 253 | }); 254 | }); 255 | --------------------------------------------------------------------------------