├── .npmrc ├── cjs ├── package.json └── index.js ├── test ├── package.json ├── index.html ├── strings.txt ├── numbers.txt ├── bench.js ├── index.js └── data.json ├── flatted.jpg ├── .gitignore ├── .npmignore ├── rollup ├── esm.config.js ├── es.config.js └── babel.config.js ├── types └── index.d.ts ├── tsconfig.json ├── .github ├── FUNDING.yml └── workflows │ ├── publish.yml │ └── node.js.yml ├── jsr.json ├── LICENSE ├── esm.js ├── es.js ├── min.js ├── python ├── test.py └── flatted.py ├── package.json ├── SPECS.md ├── esm └── index.js ├── php ├── test.php └── flatted.php ├── README.md └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /flatted.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/flatted/HEAD/flatted.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | node_modules/ 4 | coverage/ 5 | __pycache__ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .github 3 | .nyc_output 4 | .travis.yml 5 | __pycache__/ 6 | node_modules/ 7 | coverage/ 8 | rollup/ 9 | test/ 10 | php/test.php 11 | flatted.jpg 12 | package-lock.json 13 | SPECS.md 14 | tsconfig.json 15 | jsr.json 16 | -------------------------------------------------------------------------------- /rollup/esm.config.js: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | 3 | export default { 4 | input: './esm/index.js', 5 | plugins: [ 6 | terser() 7 | ], 8 | output: { 9 | file: './esm.js', 10 | format: 'module' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /rollup/es.config.js: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | 3 | export default { 4 | input: './esm/index.js', 5 | plugins: [ 6 | terser() 7 | ], 8 | output: { 9 | esModule: false, 10 | exports: 'named', 11 | file: './es.js', 12 | format: 'iife', 13 | name: 'Flatted' 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /rollup/babel.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | 3 | export default { 4 | input: './esm/index.js', 5 | plugins: [ 6 | babel({presets: ['@babel/preset-env']}) 7 | ], 8 | output: { 9 | esModule: false, 10 | exports: 'named', 11 | file: './index.js', 12 | format: 'iife', 13 | name: 'Flatted' 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export function parse(text: string, reviver?: (this: any, key: string, value: any) => any): any; 2 | export function stringify(value: any, replacer?: (string | number)[] | ((this: any, key: string, value: any) => any), space?: string | number | undefined): string; 3 | export function toJSON(value: any): any; 4 | export function fromJSON(value: any): any; 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "esnext", 5 | "moduleResolution": "nodenext", 6 | "allowJs": true, 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "declarationDir": "types" 10 | }, 11 | "include": [ 12 | "esm/index.js" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # not working due missing www. 5 | open_collective: # 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: https://www.patreon.com/webreflection 9 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wr/flatted", 3 | "version": "3.3.1", 4 | "exports": "./esm/index.js", 5 | "exclude": [ 6 | ".github/*", 7 | "./cjs/*", 8 | "./coverage/*", 9 | "./node_modules/*", 10 | "./rollup/*", 11 | "./test/*", 12 | ".gitignore", 13 | ".npmignore", 14 | ".npmrc", 15 | "es.js", 16 | "index.js", 17 | "min.js", 18 | "package-lock.json", 19 | "tsconfig.json" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install Deno 19 | uses: denoland/setup-deno@v1 20 | 21 | - name: Publish package 22 | run: deno publish --config jsr.json 23 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | flatted 7 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018-2020, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [24] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | - run: npm ci 25 | - run: npm run build --if-present 26 | - run: npm test 27 | - run: npm run coverage --if-present 28 | - name: Coveralls 29 | uses: coverallsapp/github-action@master 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /esm.js: -------------------------------------------------------------------------------- 1 | const{parse:t,stringify:e}=JSON,{keys:n}=Object,l=String,o="string",r={},s="object",c=(t,e)=>e,a=t=>t instanceof l?l(t):t,f=(t,e)=>typeof e===o?new l(e):e,i=(t,e,o,c)=>{const a=[];for(let f=n(o),{length:i}=f,p=0;p{const o=l(e.push(n)-1);return t.set(n,o),o},u=(e,n)=>{const l=t(e,f).map(a),o=l[0],r=n||c,p=typeof o===s&&o?i(l,new Set,o,r):o;return r.call({"":p},"",p)},h=(t,n,l)=>{const r=n&&typeof n===s?(t,e)=>""===t||-1t(h(e)),g=t=>u(e(t));export{g as fromJSON,u as parse,h as stringify,y as toJSON}; 2 | -------------------------------------------------------------------------------- /es.js: -------------------------------------------------------------------------------- 1 | self.Flatted=function(t){"use strict";const{parse:e,stringify:n}=JSON,{keys:r}=Object,s=String,o="string",c={},l="object",a=(t,e)=>e,f=t=>t instanceof s?s(t):t,i=(t,e)=>typeof e===o?new s(e):e,u=(t,e,n,o)=>{const a=[];for(let f=r(n),{length:i}=f,u=0;u{const r=s(e.push(n)-1);return t.set(n,r),r},y=(t,n)=>{const r=e(t,i).map(f),s=r[0],o=n||a,c=typeof s===l&&s?u(r,new Set,s,o):s;return o.call({"":c},"",c)},g=(t,e,r)=>{const s=e&&typeof e===l?(t,n)=>""===t||-1y(n(t)),t.parse=y,t.stringify=g,t.toJSON=t=>e(g(t)),t}({}); 2 | -------------------------------------------------------------------------------- /min.js: -------------------------------------------------------------------------------- 1 | self.Flatted=function(n){"use strict";function t(n){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n},t(n)}var r=JSON.parse,e=JSON.stringify,o=Object.keys,u=String,f="string",i={},c="object",a=function(n,t){return t},l=function(n){return n instanceof u?u(n):n},s=function(n,r){return t(r)===f?new u(r):r},y=function(n,r,e,f){for(var a=[],l=o(e),s=l.length,p=0;p ./coverage/lcov.info" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/WebReflection/flatted.git" 24 | }, 25 | "files": [ 26 | "LICENSE", 27 | "README.md", 28 | "cjs/", 29 | "es.js", 30 | "esm.js", 31 | "esm/", 32 | "index.js", 33 | "min.js", 34 | "php/flatted.php", 35 | "python/flatted.py", 36 | "types/" 37 | ], 38 | "keywords": [ 39 | "circular", 40 | "JSON", 41 | "fast", 42 | "parser", 43 | "minimal" 44 | ], 45 | "author": "Andrea Giammarchi", 46 | "license": "ISC", 47 | "bugs": { 48 | "url": "https://github.com/WebReflection/flatted/issues" 49 | }, 50 | "homepage": "https://github.com/WebReflection/flatted#readme", 51 | "devDependencies": { 52 | "@babel/core": "^7.27.1", 53 | "@babel/preset-env": "^7.27.2", 54 | "@rollup/plugin-babel": "^6.0.4", 55 | "@rollup/plugin-terser": "^0.4.4", 56 | "@ungap/structured-clone": "^1.3.0", 57 | "ascjs": "^6.0.3", 58 | "c8": "^10.1.3", 59 | "circular-json": "^0.5.9", 60 | "circular-json-es6": "^2.0.2", 61 | "jsan": "^3.1.14", 62 | "rollup": "^4.41.1", 63 | "terser": "^5.39.2", 64 | "typescript": "^5.8.3" 65 | }, 66 | "module": "./esm/index.js", 67 | "type": "module", 68 | "exports": { 69 | ".": { 70 | "types": "./types/index.d.ts", 71 | "import": "./esm/index.js", 72 | "default": "./cjs/index.js" 73 | }, 74 | "./esm": "./esm.js", 75 | "./package.json": "./package.json" 76 | }, 77 | "types": "./types/index.d.ts" 78 | } 79 | -------------------------------------------------------------------------------- /test/strings.txt: -------------------------------------------------------------------------------- 1 | ----------------------------------- 2 | Object with 7 keys each 3 | ----------------------------------- 4 | CircularJSON 100 objects parsed 5.95 times per second 5 | CircularJSON 15481 chars parsed 2.83 times per second 6 | CircularJSON 50 objects parsed 12.38 times per second 7 | CircularJSON 7698 chars parsed 5.64 times per second 8 | CircularJSON 10 objects parsed 58.88 times per second 9 | CircularJSON 1515 chars parsed 26.56 times per second 10 | ----------------------------------- 11 | circular-json-es6 100 objects parsed 21.30 times per second 12 | circular-json-es6 15481 chars parsed 12.73 times per second 13 | circular-json-es6 50 objects parsed 41.54 times per second 14 | circular-json-es6 7698 chars parsed 25.40 times per second 15 | circular-json-es6 10 objects parsed 180.76 times per second 16 | circular-json-es6 1515 chars parsed 121.09 times per second 17 | ----------------------------------- 18 | jsan 100 objects parsed 21.35 times per second 19 | jsan 15481 chars parsed 11.49 times per second 20 | jsan 50 objects parsed 41.91 times per second 21 | jsan 7698 chars parsed 23.01 times per second 22 | jsan 10 objects parsed 179.26 times per second 23 | jsan 1515 chars parsed 108.14 times per second 24 | ----------------------------------- 25 | flatted 100 objects parsed 5.27 times per second 26 | flatted 11695 chars parsed 1.66 times per second 27 | flatted 50 objects parsed 9.63 times per second 28 | flatted 5857 chars parsed 3.24 times per second 29 | flatted 10 objects parsed 37.45 times per second 30 | flatted 1336 chars parsed 14.84 times per second 31 | ----------------------------------- 32 | 50% same objects 33 | ----------------------------------- 34 | CircularJSON 100 objects parsed 9.85 times per second 35 | CircularJSON 7988 chars parsed 4.22 times per second 36 | circular-json-es6 100 objects parsed 21.53 times per second 37 | circular-json-es6 15395 chars parsed 12.87 times per second 38 | jsan 100 objects parsed 21.51 times per second 39 | jsan 15395 chars parsed 11.41 times per second 40 | flatted 100 objects parsed 8.96 times per second 41 | flatted 6098 chars parsed 2.84 times per second 42 | ----------------------------------- 43 | 90% same objects 44 | ----------------------------------- 45 | CircularJSON 100 objects parsed 17.95 times per second 46 | CircularJSON 1965 chars parsed 9.52 times per second 47 | circular-json-es6 100 objects parsed 21.43 times per second 48 | circular-json-es6 15141 chars parsed 12.79 times per second 49 | jsan 100 objects parsed 21.82 times per second 50 | jsan 15141 chars parsed 11.71 times per second 51 | flatted 100 objects parsed 25.65 times per second 52 | flatted 1705 chars parsed 7.96 times per second 53 | ----------------------------------- 54 | with circular 55 | ----------------------------------- 56 | CircularJSON 100 objects parsed 12.15 times per second 57 | CircularJSON 1191 chars parsed 6.13 times per second 58 | circular-json-es6 100 objects parsed 37.64 times per second 59 | circular-json-es6 1188 chars parsed 52.54 times per second 60 | jsan 100 objects parsed 19.40 times per second 61 | jsan 2391 chars parsed 15.57 times per second 62 | flatted 100 objects parsed 11.43 times per second 63 | flatted 1587 chars parsed 5.06 times per second 64 | ----------------------------------- 65 | with circular 90% same 66 | ----------------------------------- 67 | CircularJSON 100 objects parsed 21.75 times per second 68 | CircularJSON 561 chars parsed 12.35 times per second 69 | circular-json-es6 100 objects parsed 98.35 times per second 70 | circular-json-es6 295 chars parsed 246.58 times per second 71 | jsan 100 objects parsed 34.27 times per second 72 | jsan 1761 chars parsed 23.58 times per second 73 | flatted 100 objects parsed 43.85 times per second 74 | flatted 514 chars parsed 12.96 times per second -------------------------------------------------------------------------------- /test/numbers.txt: -------------------------------------------------------------------------------- 1 | ----------------------------------- 2 | Object with 7 keys each 3 | ----------------------------------- 4 | CircularJSON 100 objects parsed 5.19 times per second 5 | CircularJSON 15481 chars parsed 2.82 times per second 6 | CircularJSON 50 objects parsed 10.69 times per second 7 | CircularJSON 7698 chars parsed 5.58 times per second 8 | CircularJSON 10 objects parsed 51.15 times per second 9 | CircularJSON 1515 chars parsed 26.56 times per second 10 | ----------------------------------- 11 | circular-json-es6 100 objects parsed 21.12 times per second 12 | circular-json-es6 15481 chars parsed 12.73 times per second 13 | circular-json-es6 50 objects parsed 41.70 times per second 14 | circular-json-es6 7698 chars parsed 25.41 times per second 15 | circular-json-es6 10 objects parsed 181.25 times per second 16 | circular-json-es6 1515 chars parsed 117.90 times per second 17 | ----------------------------------- 18 | jsan 100 objects parsed 21.21 times per second 19 | jsan 15481 chars parsed 11.44 times per second 20 | jsan 50 objects parsed 41.30 times per second 21 | jsan 7698 chars parsed 22.96 times per second 22 | jsan 10 objects parsed 180.61 times per second 23 | jsan 1515 chars parsed 107.22 times per second 24 | ----------------------------------- 25 | flatted 100 objects parsed 4.29 times per second 26 | flatted 16290 chars parsed 1.89 times per second 27 | flatted 50 objects parsed 8.23 times per second 28 | flatted 8052 chars parsed 3.69 times per second 29 | flatted 10 objects parsed 36.67 times per second 30 | flatted 1589 chars parsed 17.50 times per second 31 | ----------------------------------- 32 | 50% same objects 33 | ----------------------------------- 34 | CircularJSON 100 objects parsed 8.71 times per second 35 | CircularJSON 7988 chars parsed 4.17 times per second 36 | circular-json-es6 100 objects parsed 21.36 times per second 37 | circular-json-es6 15395 chars parsed 12.82 times per second 38 | jsan 100 objects parsed 21.40 times per second 39 | jsan 15395 chars parsed 11.60 times per second 40 | flatted 100 objects parsed 7.77 times per second 41 | flatted 8193 chars parsed 3.33 times per second 42 | ----------------------------------- 43 | 90% same objects 44 | ----------------------------------- 45 | CircularJSON 100 objects parsed 17.82 times per second 46 | CircularJSON 1965 chars parsed 9.56 times per second 47 | circular-json-es6 100 objects parsed 21.64 times per second 48 | circular-json-es6 15141 chars parsed 12.93 times per second 49 | jsan 100 objects parsed 21.76 times per second 50 | jsan 15141 chars parsed 11.60 times per second 51 | flatted 100 objects parsed 26.10 times per second 52 | flatted 1778 chars parsed 9.21 times per second 53 | ----------------------------------- 54 | with circular 55 | ----------------------------------- 56 | CircularJSON 100 objects parsed 11.57 times per second 57 | CircularJSON 1191 chars parsed 6.07 times per second 58 | circular-json-es6 100 objects parsed 37.32 times per second 59 | circular-json-es6 1188 chars parsed 51.12 times per second 60 | jsan 100 objects parsed 19.72 times per second 61 | jsan 2391 chars parsed 15.66 times per second 62 | flatted 100 objects parsed 11.81 times per second 63 | flatted 1187 chars parsed 5.75 times per second 64 | ----------------------------------- 65 | with circular 90% same 66 | ----------------------------------- 67 | CircularJSON 100 objects parsed 22.17 times per second 68 | CircularJSON 561 chars parsed 12.11 times per second 69 | circular-json-es6 100 objects parsed 99.08 times per second 70 | circular-json-es6 295 chars parsed 242.76 times per second 71 | jsan 100 objects parsed 35.49 times per second 72 | jsan 1761 chars parsed 23.64 times per second 73 | flatted 100 objects parsed 48.95 times per second 74 | flatted 294 chars parsed 14.13 times per second 75 | -------------------------------------------------------------------------------- /SPECS.md: -------------------------------------------------------------------------------- 1 | # Flatted Specifications 2 | 3 | This document describes operations performed to produce, or parse, the flatted output. 4 | 5 | ## stringify(any) => flattedString 6 | 7 | The output is always an `Array` that contains at index `0` the given value. 8 | 9 | If the value is an `Array` or an `Object`, per each property value passed through the callback, return the value as is if it's not an `Array`, an `Object`, or a `string`. 10 | 11 | In case it's an `Array`, an `Object`, or a `string`, return the index as `string`, associated through a `Map`. 12 | 13 | Giving the following example: 14 | 15 | ```js 16 | flatted.stringify('a'); // ["a"] 17 | flatted.stringify(['a']); // [["1"],"a"] 18 | flatted.stringify(['a', 1, 'b']); // [["1",1,"2"],"a","b"] 19 | ``` 20 | 21 | There is an `input` containing `[array, "a", "b"]`, where the `array` has indexes `"1"` and `"2"` as strings, indexes that point respectively at `"a"` and `"b"` within the input `[array, "a", "b"]`. 22 | 23 | The exact same happens for objects. 24 | 25 | ```js 26 | flatted.stringify('a'); // ["a"] 27 | flatted.stringify({a: 'a'}); // [{"a":"1"},"a"] 28 | flatted.stringify({a: 'a', n: 1, b: 'b'}); // [{"a":"1","n":1,"b":"2"},"a","b"] 29 | ``` 30 | 31 | Every object, string, or array, encountered during serialization will be stored once as stringified index. 32 | 33 | ```js 34 | // per each property/value of the object/array 35 | if (any == null || !/object|string/.test(typeof any)) 36 | return any; 37 | if (!map.has(any)) { 38 | const index = String(arr.length); 39 | arr.push(any); 40 | map.set(any, index); 41 | } 42 | return map.get(any); 43 | ``` 44 | 45 | This, performed before going through all properties, grants unique indexes per reference. 46 | 47 | The stringified indexes ensure there won't be conflicts with regularly stored numbers. 48 | 49 | ## parse(flattedString) => any 50 | 51 | Everything that is a `string` is wrapped as `new String`, but strings in the array, from index `1` on, is kept as regular `string`. 52 | 53 | ```js 54 | const input = JSON.parse('[{"a":"1"},"b"]', Strings).map(strings); 55 | // convert strings primitives into String instances 56 | function Strings(key, value) { 57 | return typeof value === 'string' ? new String(value) : value; 58 | } 59 | // converts String instances into strings primitives 60 | function strings(value) { 61 | return value instanceof String ? String(value) : value; 62 | } 63 | ``` 64 | 65 | The `input` array will have a regular `string` at index `1`, but its object at index `0` will have an `instanceof String` as `.a` property. 66 | 67 | That is the key to place back values from the rest of the array, so that per each property of the object at index `0`, if the value is an `instanceof` String, something not serializable via JSON, it means it can be used to retrieve the position of its value from the `input` array. 68 | 69 | If such `value` is an object and it hasn't been parsed yet, add it as parsed and go through all its properties/values. 70 | 71 | ```js 72 | // outside any loop ... 73 | const parsed = new Set; 74 | 75 | // ... per each property/value ... 76 | if (value instanceof Primitive) { 77 | const tmp = input[parseInt(value)]; 78 | if (typeof tmp === 'object' && !parsed.has(tmp)) { 79 | parsed.add(tmp); 80 | output[key] = tmp; 81 | if (typeof tmp === 'object' && tmp != null) { 82 | // perform this same logic per 83 | // each nested property/value ... 84 | } 85 | } else { 86 | output[key] = tmp; 87 | } 88 | } else 89 | output[key] = tmp; 90 | ``` 91 | 92 | As summary, the whole logic is based on polluting the de-serialization with a kind of variable that is unexpected, hence secure to use as directive to retrieve an index with a value. 93 | 94 | The usage of a `Map` and a `Set` to flag known references/strings as visited/stored makes **flatted** a rock solid, fast, and compact, solution. 95 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // (c) 2020-present Andrea Giammarchi 4 | 5 | const {parse: $parse, stringify: $stringify} = JSON; 6 | const {keys} = Object; 7 | 8 | const Primitive = String; // it could be Number 9 | const primitive = 'string'; // it could be 'number' 10 | 11 | const ignore = {}; 12 | const object = 'object'; 13 | 14 | const noop = (_, value) => value; 15 | 16 | const primitives = value => ( 17 | value instanceof Primitive ? Primitive(value) : value 18 | ); 19 | 20 | const Primitives = (_, value) => ( 21 | typeof value === primitive ? new Primitive(value) : value 22 | ); 23 | 24 | const revive = (input, parsed, output, $) => { 25 | const lazy = []; 26 | for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) { 27 | const k = ke[y]; 28 | const value = output[k]; 29 | if (value instanceof Primitive) { 30 | const tmp = input[value]; 31 | if (typeof tmp === object && !parsed.has(tmp)) { 32 | parsed.add(tmp); 33 | output[k] = ignore; 34 | lazy.push({k, a: [input, parsed, tmp, $]}); 35 | } 36 | else 37 | output[k] = $.call(output, k, tmp); 38 | } 39 | else if (output[k] !== ignore) 40 | output[k] = $.call(output, k, value); 41 | } 42 | for (let {length} = lazy, i = 0; i < length; i++) { 43 | const {k, a} = lazy[i]; 44 | output[k] = $.call(output, k, revive.apply(null, a)); 45 | } 46 | return output; 47 | }; 48 | 49 | const set = (known, input, value) => { 50 | const index = Primitive(input.push(value) - 1); 51 | known.set(value, index); 52 | return index; 53 | }; 54 | 55 | /** 56 | * Converts a specialized flatted string into a JS value. 57 | * @param {string} text 58 | * @param {(this: any, key: string, value: any) => any} [reviver] 59 | * @returns {any} 60 | */ 61 | export const parse = (text, reviver) => { 62 | const input = $parse(text, Primitives).map(primitives); 63 | const value = input[0]; 64 | const $ = reviver || noop; 65 | const tmp = typeof value === object && value ? 66 | revive(input, new Set, value, $) : 67 | value; 68 | return $.call({'': tmp}, '', tmp); 69 | }; 70 | 71 | /** 72 | * Converts a JS value into a specialized flatted string. 73 | * @param {any} value 74 | * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer] 75 | * @param {string | number | undefined} [space] 76 | * @returns {string} 77 | */ 78 | export const stringify = (value, replacer, space) => { 79 | const $ = replacer && typeof replacer === object ? 80 | (k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) : 81 | (replacer || noop); 82 | const known = new Map; 83 | const input = []; 84 | const output = []; 85 | let i = +set(known, input, $.call({'': value}, '', value)); 86 | let firstRun = !i; 87 | while (i < input.length) { 88 | firstRun = true; 89 | output[i] = $stringify(input[i++], replace, space); 90 | } 91 | return '[' + output.join(',') + ']'; 92 | function replace(key, value) { 93 | if (firstRun) { 94 | firstRun = !firstRun; 95 | return value; 96 | } 97 | const after = $.call(this, key, value); 98 | switch (typeof after) { 99 | case object: 100 | if (after === null) return after; 101 | case primitive: 102 | return known.get(after) || set(known, input, after); 103 | } 104 | return after; 105 | } 106 | }; 107 | 108 | /** 109 | * Converts a generic value into a JSON serializable object without losing recursion. 110 | * @param {any} value 111 | * @returns {any} 112 | */ 113 | export const toJSON = value => $parse(stringify(value)); 114 | 115 | /** 116 | * Converts a previously serialized object with recursion into a recursive one. 117 | * @param {any} value 118 | * @returns {any} 119 | */ 120 | export const fromJSON = value => parse($stringify(value)); 121 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /// 3 | 4 | // (c) 2020-present Andrea Giammarchi 5 | 6 | const {parse: $parse, stringify: $stringify} = JSON; 7 | const {keys} = Object; 8 | 9 | const Primitive = String; // it could be Number 10 | const primitive = 'string'; // it could be 'number' 11 | 12 | const ignore = {}; 13 | const object = 'object'; 14 | 15 | const noop = (_, value) => value; 16 | 17 | const primitives = value => ( 18 | value instanceof Primitive ? Primitive(value) : value 19 | ); 20 | 21 | const Primitives = (_, value) => ( 22 | typeof value === primitive ? new Primitive(value) : value 23 | ); 24 | 25 | const revive = (input, parsed, output, $) => { 26 | const lazy = []; 27 | for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) { 28 | const k = ke[y]; 29 | const value = output[k]; 30 | if (value instanceof Primitive) { 31 | const tmp = input[value]; 32 | if (typeof tmp === object && !parsed.has(tmp)) { 33 | parsed.add(tmp); 34 | output[k] = ignore; 35 | lazy.push({k, a: [input, parsed, tmp, $]}); 36 | } 37 | else 38 | output[k] = $.call(output, k, tmp); 39 | } 40 | else if (output[k] !== ignore) 41 | output[k] = $.call(output, k, value); 42 | } 43 | for (let {length} = lazy, i = 0; i < length; i++) { 44 | const {k, a} = lazy[i]; 45 | output[k] = $.call(output, k, revive.apply(null, a)); 46 | } 47 | return output; 48 | }; 49 | 50 | const set = (known, input, value) => { 51 | const index = Primitive(input.push(value) - 1); 52 | known.set(value, index); 53 | return index; 54 | }; 55 | 56 | /** 57 | * Converts a specialized flatted string into a JS value. 58 | * @param {string} text 59 | * @param {(this: any, key: string, value: any) => any} [reviver] 60 | * @returns {any} 61 | */ 62 | const parse = (text, reviver) => { 63 | const input = $parse(text, Primitives).map(primitives); 64 | const value = input[0]; 65 | const $ = reviver || noop; 66 | const tmp = typeof value === object && value ? 67 | revive(input, new Set, value, $) : 68 | value; 69 | return $.call({'': tmp}, '', tmp); 70 | }; 71 | exports.parse = parse; 72 | 73 | /** 74 | * Converts a JS value into a specialized flatted string. 75 | * @param {any} value 76 | * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer] 77 | * @param {string | number | undefined} [space] 78 | * @returns {string} 79 | */ 80 | const stringify = (value, replacer, space) => { 81 | const $ = replacer && typeof replacer === object ? 82 | (k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) : 83 | (replacer || noop); 84 | const known = new Map; 85 | const input = []; 86 | const output = []; 87 | let i = +set(known, input, $.call({'': value}, '', value)); 88 | let firstRun = !i; 89 | while (i < input.length) { 90 | firstRun = true; 91 | output[i] = $stringify(input[i++], replace, space); 92 | } 93 | return '[' + output.join(',') + ']'; 94 | function replace(key, value) { 95 | if (firstRun) { 96 | firstRun = !firstRun; 97 | return value; 98 | } 99 | const after = $.call(this, key, value); 100 | switch (typeof after) { 101 | case object: 102 | if (after === null) return after; 103 | case primitive: 104 | return known.get(after) || set(known, input, after); 105 | } 106 | return after; 107 | } 108 | }; 109 | exports.stringify = stringify; 110 | 111 | /** 112 | * Converts a generic value into a JSON serializable object without losing recursion. 113 | * @param {any} value 114 | * @returns {any} 115 | */ 116 | const toJSON = value => $parse(stringify(value)); 117 | exports.toJSON = toJSON; 118 | 119 | /** 120 | * Converts a previously serialized object with recursion into a recursive one. 121 | * @param {any} value 122 | * @returns {any} 123 | */ 124 | const fromJSON = value => parse($stringify(value)); 125 | exports.fromJSON = fromJSON; 126 | -------------------------------------------------------------------------------- /python/flatted.py: -------------------------------------------------------------------------------- 1 | # ISC License 2 | # 3 | # Copyright (c) 2018-2025, Andrea Giammarchi, @WebReflection 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | # PERFORMANCE OF THIS SOFTWARE. 16 | 17 | import json as _json 18 | 19 | class _Known: 20 | def __init__(self): 21 | self.key = [] 22 | self.value = [] 23 | 24 | class _String: 25 | def __init__(self, value): 26 | self.value = value 27 | 28 | 29 | def _array_keys(value): 30 | keys = [] 31 | i = 0 32 | for _ in value: 33 | keys.append(i) 34 | i += 1 35 | return keys 36 | 37 | def _object_keys(value): 38 | keys = [] 39 | for key in value: 40 | keys.append(key) 41 | return keys 42 | 43 | def _is_array(value): 44 | return isinstance(value, (list, tuple)) 45 | 46 | def _is_object(value): 47 | return isinstance(value, dict) 48 | 49 | def _is_string(value): 50 | return isinstance(value, str) 51 | 52 | def _index(known, input, value): 53 | input.append(value) 54 | index = str(len(input) - 1) 55 | known.key.append(value) 56 | known.value.append(index) 57 | return index 58 | 59 | def _loop(keys, input, known, output): 60 | for key in keys: 61 | value = output[key] 62 | if isinstance(value, _String): 63 | _ref(key, input[int(value.value)], input, known, output) 64 | 65 | return output 66 | 67 | def _ref(key, value, input, known, output): 68 | if _is_array(value) and value not in known: 69 | known.append(value) 70 | value = _loop(_array_keys(value), input, known, value) 71 | elif _is_object(value) and value not in known: 72 | known.append(value) 73 | value = _loop(_object_keys(value), input, known, value) 74 | 75 | output[key] = value 76 | 77 | def _relate(known, input, value): 78 | if _is_string(value) or _is_array(value) or _is_object(value): 79 | try: 80 | return known.value[known.key.index(value)] 81 | except: 82 | return _index(known, input, value) 83 | 84 | return value 85 | 86 | def _transform(known, input, value): 87 | if _is_array(value): 88 | output = [] 89 | for val in value: 90 | output.append(_relate(known, input, val)) 91 | return output 92 | 93 | if _is_object(value): 94 | obj = {} 95 | for key in value: 96 | obj[key] = _relate(known, input, value[key]) 97 | return obj 98 | 99 | return value 100 | 101 | def _wrap(value): 102 | if _is_string(value): 103 | return _String(value) 104 | 105 | if _is_array(value): 106 | i = 0 107 | for val in value: 108 | value[i] = _wrap(val) 109 | i += 1 110 | 111 | elif _is_object(value): 112 | for key in value: 113 | value[key] = _wrap(value[key]) 114 | 115 | return value 116 | 117 | def parse(value, *args, **kwargs): 118 | json = _json.loads(value, *args, **kwargs) 119 | wrapped = [] 120 | for value in json: 121 | wrapped.append(_wrap(value)) 122 | 123 | input = [] 124 | for value in wrapped: 125 | if isinstance(value, _String): 126 | input.append(value.value) 127 | else: 128 | input.append(value) 129 | 130 | value = input[0] 131 | 132 | if _is_array(value): 133 | return _loop(_array_keys(value), input, [value], value) 134 | 135 | if _is_object(value): 136 | return _loop(_object_keys(value), input, [value], value) 137 | 138 | return value 139 | 140 | 141 | def stringify(value, *args, **kwargs): 142 | known = _Known() 143 | input = [] 144 | output = [] 145 | i = int(_index(known, input, value)) 146 | while i < len(input): 147 | output.append(_transform(known, input, input[i])) 148 | i += 1 149 | return _json.dumps(output, *args, **kwargs) 150 | -------------------------------------------------------------------------------- /php/test.php: -------------------------------------------------------------------------------- 1 | o = &$o; 26 | 27 | console::assert(Flatted::stringify($a) === '[["0"]]', 'recursive Array'); 28 | console::assert(Flatted::stringify($o) === '[{"o":"0"}]', 'recursive Object'); 29 | 30 | $b = Flatted::parse(Flatted::stringify($a)); 31 | console::assert(is_array($b) && $b[0] === $b, 'restoring recursive Array'); 32 | 33 | $a[] = 1; 34 | $a[] = 'two'; 35 | $a[] = true; 36 | $o->one = 1; 37 | $o->two = 'two'; 38 | $o->three = true; 39 | 40 | console::assert(Flatted::stringify($a) === '[["0",1,"1",true],"two"]', 'values in Array'); 41 | console::assert(Flatted::stringify($o) === '[{"o":"0","one":1,"two":"1","three":true},"two"]', 'values in Object'); 42 | 43 | $a[] = &$o; 44 | $o->a = &$a; 45 | 46 | console::assert(Flatted::stringify($a) === '[["0",1,"1",true,"2"],"two",{"o":"2","one":1,"two":"1","three":true,"a":"0"}]', 'object in Array'); 47 | console::assert(Flatted::stringify($o) === '[{"o":"0","one":1,"two":"1","three":true,"a":"2"},"two",["2",1,"1",true,"0"]]', 'array in Object'); 48 | 49 | $a[] = array('test' => 'OK'); 50 | $a[] = [1, 2, 3]; 51 | 52 | $o->test = array('test' => 'OK'); 53 | $o->array = [1, 2, 3]; 54 | 55 | console::assert(Flatted::stringify($a) === '[["0",1,"1",true,"2","3","4"],"two",{"o":"2","one":1,"two":"1","three":true,"a":"0","test":"3","array":"4"},{"test":"5"},[1,2,3],"OK"]', 'objects in Array'); 56 | console::assert(Flatted::stringify($o) === '[{"o":"0","one":1,"two":"1","three":true,"a":"2","test":"3","array":"4"},"two",["2",1,"1",true,"0","3","4"],{"test":"5"},[1,2,3],"OK"]', 'objects in Object'); 57 | 58 | $a2 = Flatted::parse(Flatted::stringify($a)); 59 | $o2 = Flatted::parse(Flatted::stringify($o)); 60 | 61 | console::assert($a2[0] === $a2, 'parsed Array'); 62 | console::assert($o2->o === $o2, 'parsed Object'); 63 | 64 | console::assert( 65 | $a2[1] === 1 && 66 | $a2[2] === 'two' && 67 | $a2[3] === true && 68 | $a2[4] instanceof stdClass && 69 | json_encode($a2[5]) === json_encode(array('test' => 'OK')) && 70 | json_encode($a2[6]) === json_encode([1, 2, 3]), 71 | 'array values are all OK' 72 | ); 73 | 74 | console::assert($a2[4] === $a2[4]->o && $a2 === $a2[4]->o->a, 'array recursive values are OK'); 75 | 76 | console::assert( 77 | $o2->one === 1 && 78 | $o2->two === 'two' && 79 | $o2->three === true && 80 | is_array($o2->a) && 81 | json_encode($o2->test) === json_encode(array('test' => 'OK')) && 82 | json_encode($o2->array) === json_encode([1, 2, 3]), 83 | 'object values are all OK' 84 | ); 85 | 86 | console::assert($o2->a === $o2->a[0] && $o2 === $o2->a[4], 'object recursive values are OK'); 87 | 88 | console::assert(Flatted::parse(Flatted::stringify(1)) === 1, 'numbers can be parsed too'); 89 | console::assert(Flatted::parse(Flatted::stringify(false)) === false, 'booleans can be parsed too'); 90 | console::assert(Flatted::parse(Flatted::stringify(null)) === null, 'null can be parsed too'); 91 | console::assert(Flatted::parse(Flatted::stringify('test')) === 'test', 'strings can be parsed too'); 92 | 93 | $str = Flatted::parse('[{"prop":"1","a":"2","b":"3"},{"value":123},["4","5"],{"e":"6","t":"7","p":4},{},{"b":"8"},"f",{"a":"9"},["10"],"sup",{"a":1,"d":2,"c":"7","z":"11","h":1},{"g":2,"a":"7","b":"12","f":6},{"r":4,"u":"7","c":5}]'); 94 | 95 | console::assert( 96 | $str->b->t->a === 'sup' && 97 | $str->a[1]->b[0]->c === $str->b->t, 98 | 'str is fine' 99 | ); 100 | 101 | $oo = Flatted::parse('[{"a":"1","b":"0","c":"2"},{"aa":"3"},{"ca":"4","cb":"5","cc":"6","cd":"7","ce":"8","cf":"9"},{"aaa":"10"},{"caa":"4"},{"cba":"5"},{"cca":"2"},{"cda":"4"},"value2","value3","value1"]'); 102 | 103 | console::assert( 104 | $oo->a->aa->aaa === 'value1' 105 | && $oo === $oo->b 106 | && $oo->c->ca->caa === $oo->c->ca 107 | && $oo->c->cb->cba === $oo->c->cb 108 | && $oo->c->cc->cca === $oo->c 109 | && $oo->c->cd->cda === $oo->c->ca->caa 110 | && $oo->c->ce === 'value2' 111 | && $oo->c->cf === 'value3', 112 | 'parse is correct' 113 | ); 114 | 115 | echo "OK\n"; 116 | 117 | ?> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flatted 2 | 3 | [![Downloads](https://img.shields.io/npm/dm/flatted.svg)](https://www.npmjs.com/package/flatted) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/flatted/badge.svg?branch=main)](https://coveralls.io/github/WebReflection/flatted?branch=main) [![License: ISC](https://img.shields.io/badge/License-ISC-yellow.svg)](https://opensource.org/licenses/ISC) ![WebReflection status](https://offline.report/status/webreflection.svg) 4 | 5 | ![snow flake](./flatted.jpg) 6 | 7 | **Social Media Photo by [Matt Seymour](https://unsplash.com/@mattseymour) on [Unsplash](https://unsplash.com/)** 8 | 9 | A super light (0.5K) and fast circular JSON parser, directly from the creator of [CircularJSON](https://github.com/WebReflection/circular-json/#circularjson). 10 | 11 | Available also for **[PHP](./php/flatted.php)**. 12 | 13 | Available also for **[Python](./python/flatted.py)**. 14 | 15 | - - - 16 | 17 | ## Announcement 📣 18 | 19 | There is a standard approach to recursion and more data-types than what JSON allows, and it's part of the [Structured Clone polyfill](https://github.com/ungap/structured-clone/#readme). 20 | 21 | Beside acting as a polyfill, its `@ungap/structured-clone/json` export provides both `stringify` and `parse`, and it's been tested for being faster than *flatted*, but its produced output is also smaller than *flatted* in general. 22 | 23 | The *@ungap/structured-clone* module is, in short, a drop in replacement for *flatted*, but it's not compatible with *flatted* specialized syntax. 24 | 25 | However, if recursion, as well as more data-types, are what you are after, or interesting for your projects/use cases, consider switching to this new module whenever you can 👍 26 | 27 | - - - 28 | 29 | ```js 30 | npm i flatted 31 | ``` 32 | 33 | Usable via [CDN](https://unpkg.com/flatted) or as regular module. 34 | 35 | ```js 36 | // ESM 37 | import {parse, stringify, toJSON, fromJSON} from 'flatted'; 38 | 39 | // CJS 40 | const {parse, stringify, toJSON, fromJSON} = require('flatted'); 41 | 42 | const a = [{}]; 43 | a[0].a = a; 44 | a.push(a); 45 | 46 | stringify(a); // [["1","0"],{"a":"0"}] 47 | ``` 48 | 49 | ## toJSON and fromJSON 50 | 51 | If you'd like to implicitly survive JSON serialization, these two helpers helps: 52 | 53 | ```js 54 | import {toJSON, fromJSON} from 'flatted'; 55 | 56 | class RecursiveMap extends Map { 57 | static fromJSON(any) { 58 | return new this(fromJSON(any)); 59 | } 60 | toJSON() { 61 | return toJSON([...this.entries()]); 62 | } 63 | } 64 | 65 | const recursive = new RecursiveMap; 66 | const same = {}; 67 | same.same = same; 68 | recursive.set('same', same); 69 | 70 | const asString = JSON.stringify(recursive); 71 | const asMap = RecursiveMap.fromJSON(JSON.parse(asString)); 72 | asMap.get('same') === asMap.get('same').same; 73 | // true 74 | ``` 75 | 76 | 77 | ## Flatted VS JSON 78 | 79 | As it is for every other specialized format capable of serializing and deserializing circular data, you should never `JSON.parse(Flatted.stringify(data))`, and you should never `Flatted.parse(JSON.stringify(data))`. 80 | 81 | The only way this could work is to `Flatted.parse(Flatted.stringify(data))`, as it is also for _CircularJSON_ or any other, otherwise there's no granted data integrity. 82 | 83 | Also please note this project serializes and deserializes only data compatible with JSON, so that sockets, or anything else with internal classes different from those allowed by JSON standard, won't be serialized and unserialized as expected. 84 | 85 | 86 | ### New in V1: Exact same JSON API 87 | 88 | * Added a [reviver](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Syntax) parameter to `.parse(string, reviver)` and revive your own objects. 89 | * Added a [replacer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Syntax) and a `space` parameter to `.stringify(object, replacer, space)` for feature parity with JSON signature. 90 | 91 | 92 | ### Compatibility 93 | All ECMAScript engines compatible with `Map`, `Set`, `Object.keys`, and `Array.prototype.reduce` will work, even if polyfilled. 94 | 95 | 96 | ### How does it work ? 97 | While stringifying, all Objects, including Arrays, and strings, are flattened out and replaced as unique index. `*` 98 | 99 | Once parsed, all indexes will be replaced through the flattened collection. 100 | 101 | `*` represented as string to avoid conflicts with numbers 102 | 103 | ```js 104 | // logic example 105 | var a = [{one: 1}, {two: '2'}]; 106 | a[0].a = a; 107 | // a is the main object, will be at index '0' 108 | // {one: 1} is the second object, index '1' 109 | // {two: '2'} the third, in '2', and it has a string 110 | // which will be found at index '3' 111 | 112 | Flatted.stringify(a); 113 | // [["1","2"],{"one":1,"a":"0"},{"two":"3"},"2"] 114 | // a[one,two] {one: 1, a} {two: '2'} '2' 115 | ``` 116 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | self.Flatted = (function (exports) { 2 | 'use strict'; 3 | 4 | function _typeof(o) { 5 | "@babel/helpers - typeof"; 6 | 7 | return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { 8 | return typeof o; 9 | } : function (o) { 10 | return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; 11 | }, _typeof(o); 12 | } 13 | 14 | /// 15 | 16 | // (c) 2020-present Andrea Giammarchi 17 | 18 | var $parse = JSON.parse, 19 | $stringify = JSON.stringify; 20 | var keys = Object.keys; 21 | var Primitive = String; // it could be Number 22 | var primitive = 'string'; // it could be 'number' 23 | 24 | var ignore = {}; 25 | var object = 'object'; 26 | var noop = function noop(_, value) { 27 | return value; 28 | }; 29 | var primitives = function primitives(value) { 30 | return value instanceof Primitive ? Primitive(value) : value; 31 | }; 32 | var Primitives = function Primitives(_, value) { 33 | return _typeof(value) === primitive ? new Primitive(value) : value; 34 | }; 35 | var _revive = function revive(input, parsed, output, $) { 36 | var lazy = []; 37 | for (var ke = keys(output), length = ke.length, y = 0; y < length; y++) { 38 | var k = ke[y]; 39 | var value = output[k]; 40 | if (value instanceof Primitive) { 41 | var tmp = input[value]; 42 | if (_typeof(tmp) === object && !parsed.has(tmp)) { 43 | parsed.add(tmp); 44 | output[k] = ignore; 45 | lazy.push({ 46 | k: k, 47 | a: [input, parsed, tmp, $] 48 | }); 49 | } else output[k] = $.call(output, k, tmp); 50 | } else if (output[k] !== ignore) output[k] = $.call(output, k, value); 51 | } 52 | for (var _length = lazy.length, i = 0; i < _length; i++) { 53 | var _lazy$i = lazy[i], 54 | _k = _lazy$i.k, 55 | a = _lazy$i.a; 56 | output[_k] = $.call(output, _k, _revive.apply(null, a)); 57 | } 58 | return output; 59 | }; 60 | var set = function set(known, input, value) { 61 | var index = Primitive(input.push(value) - 1); 62 | known.set(value, index); 63 | return index; 64 | }; 65 | 66 | /** 67 | * Converts a specialized flatted string into a JS value. 68 | * @param {string} text 69 | * @param {(this: any, key: string, value: any) => any} [reviver] 70 | * @returns {any} 71 | */ 72 | var parse = function parse(text, reviver) { 73 | var input = $parse(text, Primitives).map(primitives); 74 | var value = input[0]; 75 | var $ = reviver || noop; 76 | var tmp = _typeof(value) === object && value ? _revive(input, new Set(), value, $) : value; 77 | return $.call({ 78 | '': tmp 79 | }, '', tmp); 80 | }; 81 | 82 | /** 83 | * Converts a JS value into a specialized flatted string. 84 | * @param {any} value 85 | * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer] 86 | * @param {string | number | undefined} [space] 87 | * @returns {string} 88 | */ 89 | var stringify = function stringify(value, replacer, space) { 90 | var $ = replacer && _typeof(replacer) === object ? function (k, v) { 91 | return k === '' || -1 < replacer.indexOf(k) ? v : void 0; 92 | } : replacer || noop; 93 | var known = new Map(); 94 | var input = []; 95 | var output = []; 96 | var i = +set(known, input, $.call({ 97 | '': value 98 | }, '', value)); 99 | var firstRun = !i; 100 | while (i < input.length) { 101 | firstRun = true; 102 | output[i] = $stringify(input[i++], replace, space); 103 | } 104 | return '[' + output.join(',') + ']'; 105 | function replace(key, value) { 106 | if (firstRun) { 107 | firstRun = !firstRun; 108 | return value; 109 | } 110 | var after = $.call(this, key, value); 111 | switch (_typeof(after)) { 112 | case object: 113 | if (after === null) return after; 114 | case primitive: 115 | return known.get(after) || set(known, input, after); 116 | } 117 | return after; 118 | } 119 | }; 120 | 121 | /** 122 | * Converts a generic value into a JSON serializable object without losing recursion. 123 | * @param {any} value 124 | * @returns {any} 125 | */ 126 | var toJSON = function toJSON(value) { 127 | return $parse(stringify(value)); 128 | }; 129 | 130 | /** 131 | * Converts a previously serialized object with recursion into a recursive one. 132 | * @param {any} value 133 | * @returns {any} 134 | */ 135 | var fromJSON = function fromJSON(value) { 136 | return parse($stringify(value)); 137 | }; 138 | 139 | exports.fromJSON = fromJSON; 140 | exports.parse = parse; 141 | exports.stringify = stringify; 142 | exports.toJSON = toJSON; 143 | 144 | return exports; 145 | 146 | })({}); 147 | -------------------------------------------------------------------------------- /php/flatted.php: -------------------------------------------------------------------------------- 1 | value = $value; 25 | } 26 | } 27 | 28 | class Flatted { 29 | 30 | // public utilities 31 | public static function parse($json, $assoc = false, $depth = 512, $options = 0) { 32 | $input = array_map( 33 | 'Flatted::asString', 34 | array_map( 35 | 'Flatted::wrap', 36 | json_decode($json, $assoc, $depth, $options) 37 | ) 38 | ); 39 | $value = &$input[0]; 40 | $set = array(); 41 | $set[] = &$value; 42 | if (is_array($value)) 43 | return Flatted::loop(false, array_keys($value), $input, $set, $value); 44 | if (is_object($value)) 45 | return Flatted::loop(true, Flatted::keys($value), $input, $set, $value); 46 | return $value; 47 | } 48 | 49 | public static function stringify($value, $options = 0, $depth = 512) { 50 | $known = new stdClass; 51 | $known->key = array(); 52 | $known->value = array(); 53 | $input = array(); 54 | $output = array(); 55 | $i = intval(Flatted::index($known, $input, $value)); 56 | while ($i < count($input)) { 57 | $output[$i] = Flatted::transform($known, $input, $input[$i]); 58 | $i++; 59 | } 60 | return json_encode($output, $options, $depth); 61 | } 62 | 63 | // private helpers 64 | private static function asString($value) { 65 | return $value instanceof FlattedString ? $value->value : $value; 66 | } 67 | 68 | private static function index(&$known, &$input, &$value) { 69 | $input[] = &$value; 70 | $index = strval(count($input) - 1); 71 | $known->key[] = &$value; 72 | $known->value[] = &$index; 73 | return $index; 74 | } 75 | 76 | private static function keys(&$value) { 77 | $obj = new ReflectionObject($value); 78 | $props = $obj->getProperties(); 79 | $keys = array(); 80 | foreach ($props as $prop) 81 | $keys[] = $prop->getName(); 82 | return $keys; 83 | } 84 | 85 | private static function loop($obj, $keys, &$input, &$set, &$output) { 86 | foreach ($keys as $key) { 87 | $value = $obj ? $output->$key : $output[$key]; 88 | if ($value instanceof FlattedString) 89 | Flatted::ref($obj, $key, $input[$value->value], $input, $set, $output); 90 | } 91 | return $output; 92 | } 93 | 94 | private static function relate(&$known, &$input, &$value) { 95 | if (is_string($value) || is_array($value) || is_object($value)) { 96 | $key = array_search($value, $known->key, true); 97 | if ($key !== false) 98 | return $known->value[$key]; 99 | return Flatted::index($known, $input, $value); 100 | } 101 | return $value; 102 | } 103 | 104 | private static function ref($obj, &$key, &$value, &$input, &$set, &$output) { 105 | if (is_array($value) && !in_array($value, $set, true)) { 106 | $set[] = $value; 107 | $value = Flatted::loop(false, array_keys($value), $input, $set, $value); 108 | } 109 | elseif (is_object($value) && !in_array($value, $set, true)) { 110 | $set[] = $value; 111 | $value = Flatted::loop(true, Flatted::keys($value), $input, $set, $value); 112 | } 113 | if ($obj) { 114 | $output->$key = &$value; 115 | } 116 | else { 117 | $output[$key] = &$value; 118 | } 119 | } 120 | 121 | private static function transform(&$known, &$input, &$value) { 122 | if (is_array($value)) { 123 | return array_map( 124 | function ($value) use(&$known, &$input) { 125 | return Flatted::relate($known, $input, $value); 126 | }, 127 | $value 128 | ); 129 | } 130 | if (is_object($value)) { 131 | $object = new stdClass; 132 | $keys = Flatted::keys($value); 133 | foreach ($keys as $key) 134 | $object->$key = Flatted::relate($known, $input, $value->$key); 135 | return $object; 136 | } 137 | return $value; 138 | } 139 | 140 | private static function wrap($value) { 141 | if (is_string($value)) { 142 | return new FlattedString($value); 143 | } 144 | if (is_array($value)) { 145 | return array_map('Flatted::wrap', $value); 146 | } 147 | if (is_object($value)) { 148 | $keys = Flatted::keys($value); 149 | foreach ($keys as $key) { 150 | $value->$key = self::wrap($value->$key); 151 | } 152 | } 153 | return $value; 154 | } 155 | } 156 | ?> -------------------------------------------------------------------------------- /test/bench.js: -------------------------------------------------------------------------------- 1 | // original file from: 2 | // https://github.com/yyx990803/circular-json-es6 3 | 4 | var dummy100 = require('./data.json'); 5 | var dummy50 = dummy100.slice(0, 50) 6 | var dummy10 = dummy100.slice(0, 10) 7 | 8 | function bench(method, dummy) { 9 | var t = Date.now(), i = 0 10 | while ((Date.now() - t) < 1000) { 11 | r = method(dummy) 12 | i++ 13 | } 14 | return i 15 | } 16 | 17 | var r 18 | var CircularJSON = require('circular-json') 19 | var jsan = require('jsan') 20 | var cj6 = require('circular-json-es6') 21 | var flatted = require('../cjs') 22 | var SC = require('@ungap/structured-clone/json') 23 | 24 | function run(name, fn, dummy) { 25 | console.log( 26 | name + ' ' + ( 27 | typeof dummy === 'string' ? 28 | dummy.length + ' chars' : 29 | (dummy.length || '') + ' objects' 30 | ) + ' parsed ' + ( 31 | bench(fn, dummy) / 1000 32 | ).toFixed(2) + ' times per second' 33 | ) 34 | } 35 | 36 | //* 37 | console.log('-----------------------------------') 38 | console.log('Object with ' + Object.keys(dummy100[0]).length + ' keys each') 39 | console.log('-----------------------------------') 40 | run('CircularJSON', CircularJSON.stringify, dummy100) 41 | run('CircularJSON', CircularJSON.parse, r) 42 | run('CircularJSON', CircularJSON.stringify, dummy50) 43 | run('CircularJSON', CircularJSON.parse, r) 44 | run('CircularJSON', CircularJSON.stringify, dummy10) 45 | run('CircularJSON', CircularJSON.parse, r) 46 | console.log('-----------------------------------') 47 | run('circular-json-es6', cj6.stringify, dummy100) 48 | run('circular-json-es6', cj6.parse, r) 49 | run('circular-json-es6', cj6.stringify, dummy50) 50 | run('circular-json-es6', cj6.parse, r) 51 | run('circular-json-es6', cj6.stringify, dummy10) 52 | run('circular-json-es6', cj6.parse, r) 53 | console.log('-----------------------------------') 54 | run('jsan', jsan.stringify, dummy100) 55 | run('jsan', jsan.parse, r) 56 | run('jsan', jsan.stringify, dummy50) 57 | run('jsan', jsan.parse, r) 58 | run('jsan', jsan.stringify, dummy10) 59 | run('jsan', jsan.parse, r) 60 | console.log('-----------------------------------') 61 | run('flatted', flatted.stringify, dummy100) 62 | run('flatted', flatted.parse, r) 63 | run('flatted', flatted.stringify, dummy50) 64 | run('flatted', flatted.parse, r) 65 | run('flatted', flatted.stringify, dummy10) 66 | run('flatted', flatted.parse, r) 67 | console.log('-----------------------------------') 68 | run('Structured Clone', SC.stringify, dummy100) 69 | run('Structured Clone', SC.parse, r) 70 | run('Structured Clone', SC.stringify, dummy50) 71 | run('Structured Clone', SC.parse, r) 72 | run('Structured Clone', SC.stringify, dummy10) 73 | run('Structured Clone', SC.parse, r) 74 | console.log('-----------------------------------') 75 | console.log('50% same objects') 76 | dummy100 = dummy50.concat(dummy50) 77 | console.log('-----------------------------------') 78 | run('CircularJSON', CircularJSON.stringify, dummy100) 79 | run('CircularJSON', CircularJSON.parse, r) 80 | run('circular-json-es6', cj6.stringify, dummy100) 81 | run('circular-json-es6', cj6.parse, r) 82 | run('jsan', jsan.stringify, dummy100) 83 | run('jsan', jsan.parse, r) 84 | run('flatted', flatted.stringify, dummy100) 85 | run('flatted', flatted.parse, r) 86 | run('Structured Clone', SC.stringify, dummy100) 87 | run('Structured Clone', SC.parse, r) 88 | console.log('-----------------------------------') 89 | console.log('90% same objects') 90 | dummy100 = [].concat( 91 | dummy10, dummy10, dummy10, dummy10, dummy10, 92 | dummy10, dummy10, dummy10, dummy10, dummy10 93 | ) 94 | console.log('-----------------------------------') 95 | run('CircularJSON', CircularJSON.stringify, dummy100) 96 | run('CircularJSON', CircularJSON.parse, r) 97 | run('circular-json-es6', cj6.stringify, dummy100) 98 | run('circular-json-es6', cj6.parse, r) 99 | run('jsan', jsan.stringify, dummy100) 100 | run('jsan', jsan.parse, r) 101 | run('flatted', flatted.stringify, dummy100) 102 | run('flatted', flatted.parse, r) 103 | run('Structured Clone', SC.stringify, dummy100) 104 | run('Structured Clone', SC.parse, r) 105 | console.log('-----------------------------------') 106 | console.log('with circular') 107 | function makeCircularObject () { 108 | var a = {} 109 | a.b = a 110 | return a 111 | } 112 | dummy100 = [] 113 | for (var i = 0; i < 100; i++) { 114 | dummy100.push(makeCircularObject()) 115 | } 116 | console.log('-----------------------------------') 117 | run('CircularJSON', CircularJSON.stringify, dummy100) 118 | run('CircularJSON', CircularJSON.parse, r) 119 | run('circular-json-es6', cj6.stringify, dummy100) 120 | run('circular-json-es6', cj6.parse, r) 121 | run('jsan', jsan.stringify, dummy100) 122 | run('jsan', jsan.parse, r) 123 | run('flatted', flatted.stringify, dummy100) 124 | run('flatted', flatted.parse, r) 125 | run('Structured Clone', SC.stringify, dummy100) 126 | run('Structured Clone', SC.parse, r) 127 | console.log('-----------------------------------') 128 | console.log('with circular 90% same') 129 | function makeCircularObject () { 130 | var a = {} 131 | a.b = a 132 | return a 133 | } 134 | dummy10 = [] 135 | for (var i = 0; i < 10; i++) { 136 | dummy10.push(makeCircularObject()) 137 | } 138 | dummy100 = [].concat( 139 | dummy10, dummy10, dummy10, dummy10, dummy10, 140 | dummy10, dummy10, dummy10, dummy10, dummy10 141 | ) 142 | console.log('-----------------------------------') 143 | run('CircularJSON', CircularJSON.stringify, dummy100) 144 | run('CircularJSON', CircularJSON.parse, r) 145 | run('circular-json-es6', cj6.stringify, dummy100) 146 | run('circular-json-es6', cj6.parse, r) 147 | run('jsan', jsan.stringify, dummy100) 148 | run('jsan', jsan.parse, r) 149 | run('flatted', flatted.stringify, dummy100) 150 | run('flatted', flatted.parse, r) 151 | run('Structured Clone', SC.stringify, dummy100) 152 | run('Structured Clone', SC.parse, r) 153 | // */ 154 | console.log('-----------------------------------') 155 | console.log('Big real-world circular data') 156 | var cirular = CircularJSON.parse( 157 | require('fs').readFileSync( 158 | require('path').join(__dirname, './circular.txt') 159 | ).toString()); 160 | console.log('-----------------------------------') 161 | run('CircularJSON', CircularJSON.stringify, cirular) 162 | run('CircularJSON', CircularJSON.parse, r) 163 | run('circular-json-es6', cj6.stringify, cirular) 164 | run('circular-json-es6', cj6.parse, r) 165 | run('jsan', jsan.stringify, cirular) 166 | run('jsan', jsan.parse, r) 167 | run('flatted', flatted.stringify, cirular) 168 | run('flatted', flatted.parse, r) 169 | run('Structured Clone', SC.stringify, cirular) 170 | run('Structured Clone', SC.parse, r) 171 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var Flatted = require('../cjs'); 2 | 3 | console.assert(Flatted.stringify([null, null]) === '[[null,null]]', 'multiple null'); 4 | 5 | var a = []; 6 | var o = {}; 7 | 8 | console.assert(Flatted.stringify(a) === '[[]]', 'empty Array'); 9 | console.assert(Flatted.stringify(o) === '[{}]', 'empty Object'); 10 | 11 | a.push(a); 12 | o.o = o; 13 | 14 | console.assert(Flatted.stringify(a) === '[["0"]]', 'recursive Array'); 15 | console.assert(Flatted.stringify(o) === '[{"o":"0"}]', 'recursive Object'); 16 | 17 | var b = Flatted.parse(Flatted.stringify(a)); 18 | console.assert(Array.isArray(b) && b[0] === b, 'restoring recursive Array'); 19 | 20 | a.push(1, 'two', true); 21 | o.one = 1; 22 | o.two = 'two'; 23 | o.three = true; 24 | 25 | console.assert(Flatted.stringify(a) === '[["0",1,"1",true],"two"]', 'values in Array'); 26 | console.assert(Flatted.stringify(o) === '[{"o":"0","one":1,"two":"1","three":true},"two"]', 'values in Object'); 27 | 28 | 29 | a.push(o); 30 | o.a = a; 31 | 32 | console.assert(Flatted.stringify(a) === '[["0",1,"1",true,"2"],"two",{"o":"2","one":1,"two":"1","three":true,"a":"0"}]', 'object in Array'); 33 | console.assert(Flatted.stringify(o) === '[{"o":"0","one":1,"two":"1","three":true,"a":"2"},"two",["2",1,"1",true,"0"]]', 'array in Object'); 34 | 35 | a.push({test: 'OK'}, [1, 2, 3]); 36 | o.test = {test: 'OK'}; 37 | o.array = [1, 2, 3]; 38 | 39 | console.assert(Flatted.stringify(a) === '[["0",1,"1",true,"2","3","4"],"two",{"o":"2","one":1,"two":"1","three":true,"a":"0","test":"5","array":"6"},{"test":"7"},[1,2,3],{"test":"7"},[1,2,3],"OK"]', 'objects in Array'); 40 | console.assert(Flatted.stringify(o) === '[{"o":"0","one":1,"two":"1","three":true,"a":"2","test":"3","array":"4"},"two",["2",1,"1",true,"0","5","6"],{"test":"7"},[1,2,3],{"test":"7"},[1,2,3],"OK"]', 'objects in Object'); 41 | 42 | a = Flatted.parse(Flatted.stringify(a)); 43 | o = Flatted.parse(Flatted.stringify(o)); 44 | 45 | console.assert(a[0] === a, 'parsed Array'); 46 | console.assert(o.o === o, 'parsed Object'); 47 | 48 | console.assert( 49 | a[1] === 1 && 50 | a[2] === 'two' && 51 | a[3] === true && 52 | a[4] instanceof Object && 53 | JSON.stringify(a[5]) === JSON.stringify({test: 'OK'}) && 54 | JSON.stringify(a[6]) === JSON.stringify([1, 2, 3]), 55 | 'array values are all OK' 56 | ); 57 | 58 | console.assert(a[4] === a[4].o && a === a[4].o.a, 'array recursive values are OK'); 59 | 60 | console.assert( 61 | o.one === 1 && 62 | o.two === 'two' && 63 | o.three === true && 64 | Array.isArray(o.a) && 65 | JSON.stringify(o.test) === JSON.stringify({test: 'OK'}) && 66 | JSON.stringify(o.array) === JSON.stringify([1, 2, 3]), 67 | 'object values are all OK' 68 | ); 69 | 70 | console.assert(o.a === o.a[0] && o === o.a[4], 'object recursive values are OK'); 71 | 72 | console.assert(Flatted.parse(Flatted.stringify(1)) === 1, 'numbers can be parsed too'); 73 | console.assert(Flatted.parse(Flatted.stringify(false)) === false, 'booleans can be parsed too'); 74 | console.assert(Flatted.parse(Flatted.stringify(null)) === null, 'null can be parsed too'); 75 | console.assert(Flatted.parse(Flatted.stringify('test')) === 'test', 'strings can be parsed too'); 76 | 77 | var d = new Date; 78 | console.assert(Flatted.parse(Flatted.stringify(d)) === d.toISOString(), 'dates can be parsed too'); 79 | 80 | console.assert(Flatted.parse( 81 | Flatted.stringify(d), 82 | function (key, value) { 83 | if (typeof value === 'string' && /^[0-9:.ZT-]+$/.test(value)) 84 | return new Date(value); 85 | return value; 86 | } 87 | ) instanceof Date, 'dates can be revived too'); 88 | 89 | console.assert(Flatted.parse( 90 | Flatted.stringify({ 91 | sub: { 92 | one23: 123, 93 | date: d 94 | } 95 | }), 96 | function (key, value) { 97 | if (key !== '' && typeof value === 'string' && /^[0-9:.ZT-]+$/.test(value)) 98 | return new Date(value); 99 | return value; 100 | } 101 | ).sub.date instanceof Date, 'dates can be revived too'); 102 | 103 | 104 | // borrowed from CircularJSON 105 | 106 | 107 | (function () { 108 | var special = "\\x7e"; // \x7e is ~ 109 | //console.log(Flatted.stringify({a:special})); 110 | //console.log(Flatted.parse(Flatted.stringify({a:special})).a); 111 | console.assert(Flatted.parse(Flatted.stringify({a:special})).a === special, 'no problem with simulation'); 112 | special = "~\\x7e"; 113 | console.assert(Flatted.parse(Flatted.stringify({a:special})).a === special, 'no problem with special char'); 114 | }()); 115 | 116 | (function () { 117 | var o = {a: 'a', b: 'b', c: function(){}, d: {e: 123}}, 118 | a = JSON.stringify(o), 119 | b = Flatted.stringify(o); 120 | 121 | console.assert( 122 | JSON.stringify(JSON.parse(a)) === JSON.stringify(Flatted.parse(b)), 123 | 'works as JSON.parse' 124 | ); 125 | console.assert( 126 | Flatted.stringify(o, function(key, value){ 127 | if (!key || key === 'a') return value; 128 | }) === '[{"a":"1"},"a"]', 129 | 'accept callback' 130 | ); 131 | console.assert( 132 | JSON.stringify( 133 | Flatted.parse('[{"a":"1"},"a"]', function(key, value){ 134 | if (key === 'a') return 'b'; 135 | return value; 136 | }) 137 | ) === '{"a":"b"}', 138 | 'revive callback' 139 | ); 140 | }()); 141 | 142 | (function () { 143 | var o = {}, before, after; 144 | o.a = o; 145 | o.c = {}; 146 | o.d = { 147 | a: 123, 148 | b: o 149 | }; 150 | o.c.e = o; 151 | o.c.f = o.d; 152 | o.b = o.c; 153 | before = Flatted.stringify(o); 154 | o = Flatted.parse(before); 155 | console.assert( 156 | o.b === o.c && 157 | o.c.e === o && 158 | o.d.a === 123 && 159 | o.d.b === o && 160 | o.c.f === o.d && 161 | o.b === o.c, 162 | 'recreated original structure' 163 | ); 164 | }()); 165 | 166 | (function () { 167 | var o = {}; 168 | o.a = o; 169 | o.b = o; 170 | console.assert( 171 | Flatted.stringify(o, function (key, value) { 172 | if (!key || key === 'a') return value; 173 | }) === '[{"a":"0"}]', 174 | 'callback invoked' 175 | ); 176 | o = Flatted.parse('[{"a":"0"}]', function (key, value) { 177 | if (!key) { 178 | value.b = value; 179 | } 180 | return value; 181 | }); 182 | console.assert( 183 | o.a === o && o.b === o, 184 | 'reviver invoked' 185 | ); 186 | }()); 187 | 188 | (function () { 189 | var o = {}; 190 | o['~'] = o; 191 | o['\\x7e'] = '\\x7e'; 192 | o.test = '~'; 193 | 194 | o = Flatted.parse(Flatted.stringify(o)); 195 | console.assert(o['~'] === o && o.test === '~', 'still intact'); 196 | o = { 197 | a: [ 198 | '~', '~~', '~~~' 199 | ] 200 | }; 201 | o.a.push(o); 202 | o.o = o; 203 | o['~'] = o.a; 204 | o['~~'] = o.a; 205 | o['~~~'] = o.a; 206 | o = Flatted.parse(Flatted.stringify(o)); 207 | console.assert( 208 | o === o.a[3] && 209 | o === o.o && 210 | o['~'] === o.a && 211 | o['~~'] === o.a && 212 | o['~~~'] === o.a && 213 | o.a === o.a[3].a && 214 | o.a.pop() === o && 215 | o.a.join('') === '~~~~~~', 216 | 'restructured' 217 | ); 218 | 219 | }()); 220 | 221 | (function () { 222 | 223 | // make sure only own properties are parsed 224 | Object.prototype.shenanigans = true; 225 | 226 | var 227 | item = { 228 | name: 'TEST' 229 | }, 230 | original = { 231 | outer: [ 232 | { 233 | a: 'b', 234 | c: 'd', 235 | one: item, 236 | many: [item], 237 | e: 'f' 238 | } 239 | ] 240 | }, 241 | str, 242 | output 243 | ; 244 | item.value = item; 245 | str = Flatted.stringify(original); 246 | output = Flatted.parse(str); 247 | console.assert(str === '[{"outer":"1"},["2"],{"a":"3","c":"4","one":"5","many":"6","e":"7"},"b","d",{"name":"8","value":"5"},["5"],"f","TEST"]', 'string is correct'); 248 | console.assert( 249 | original.outer[0].one.name === output.outer[0].one.name && 250 | original.outer[0].many[0].name === output.outer[0].many[0].name && 251 | output.outer[0].many[0] === output.outer[0].one, 252 | 'object too' 253 | ); 254 | 255 | delete Object.prototype.shenanigans; 256 | 257 | }()); 258 | 259 | (function () { 260 | var 261 | unique = {a:'sup'}, 262 | nested = { 263 | prop: { 264 | value: 123 265 | }, 266 | a: [ 267 | {}, 268 | {b: [ 269 | { 270 | a: 1, 271 | d: 2, 272 | c: unique, 273 | z: { 274 | g: 2, 275 | a: unique, 276 | b: { 277 | r: 4, 278 | u: unique, 279 | c: 5 280 | }, 281 | f: 6 282 | }, 283 | h: 1 284 | } 285 | ]} 286 | ], 287 | b: { 288 | e: 'f', 289 | t: unique, 290 | p: 4 291 | } 292 | }, 293 | str = Flatted.stringify(nested), 294 | output 295 | ; 296 | console.assert(str === '[{"prop":"1","a":"2","b":"3"},{"value":123},["4","5"],{"e":"6","t":"7","p":4},{},{"b":"8"},"f",{"a":"9"},["10"],"sup",{"a":1,"d":2,"c":"7","z":"11","h":1},{"g":2,"a":"7","b":"12","f":6},{"r":4,"u":"7","c":5}]', 'string is OK'); 297 | output = Flatted.parse(str); 298 | console.assert(output.b.t.a === 'sup' && output.a[1].b[0].c === output.b.t, 'so is the object'); 299 | }()); 300 | 301 | (function () { 302 | var o = {bar: 'something ~ baz'}; 303 | var s = Flatted.stringify(o); 304 | console.assert(s === '[{"bar":"1"},"something ~ baz"]', 'string is correct'); 305 | var oo = Flatted.parse(s); 306 | console.assert(oo.bar === o.bar, 'parse is correct'); 307 | }()); 308 | 309 | (function () { 310 | var o = {}; 311 | o.a = { 312 | aa: { 313 | aaa: 'value1' 314 | } 315 | }; 316 | o.b = o; 317 | o.c = { 318 | ca: {}, 319 | cb: {}, 320 | cc: {}, 321 | cd: {}, 322 | ce: 'value2', 323 | cf: 'value3' 324 | }; 325 | o.c.ca.caa = o.c.ca; 326 | o.c.cb.cba = o.c.cb; 327 | o.c.cc.cca = o.c; 328 | o.c.cd.cda = o.c.ca.caa; 329 | 330 | var s = Flatted.stringify(o); 331 | console.assert(s === '[{"a":"1","b":"0","c":"2"},{"aa":"3"},{"ca":"4","cb":"5","cc":"6","cd":"7","ce":"8","cf":"9"},{"aaa":"10"},{"caa":"4"},{"cba":"5"},{"cca":"2"},{"cda":"4"},"value2","value3","value1"]', 'string is correct'); 332 | var oo = Flatted.parse(s); 333 | console.assert( 334 | oo.a.aa.aaa = 'value1' 335 | && oo === oo.b 336 | && oo.c.ca.caa === oo.c.ca 337 | && oo.c.cb.cba === oo.c.cb 338 | && oo.c.cc.cca === oo.c 339 | && oo.c.cd.cda === oo.c.ca.caa 340 | && oo.c.ce === 'value2' 341 | && oo.c.cf === 'value3', 342 | 'parse is correct' 343 | ); 344 | }()); 345 | 346 | (function () { 347 | var 348 | original = { 349 | a1: { 350 | a2: [], 351 | a3: [{name: 'whatever'}] 352 | }, 353 | a4: [] 354 | }, 355 | json, 356 | restored 357 | ; 358 | 359 | original.a1.a2[0] = original.a1; 360 | original.a4[0] = original.a1.a3[0]; 361 | 362 | json = Flatted.stringify(original); 363 | restored = Flatted.parse(json); 364 | 365 | console.assert(restored.a1.a2[0] === restored.a1, '~a1~a2~0 === ~a1'); 366 | console.assert(restored.a4[0] = restored.a1.a3[0], '~a4 === ~a1~a3~0'); 367 | }()); 368 | 369 | if (typeof Symbol !== 'undefined') { 370 | (function () { 371 | var o = {a: 1}; 372 | var a = [1, Symbol('test'), 2]; 373 | o[Symbol('test')] = 123; 374 | console.assert(('[' + JSON.stringify(o) + ']') === Flatted.stringify(o), 'Symbol is OK too'); 375 | console.assert(('[' + JSON.stringify(a) + ']') === Flatted.stringify(a), 'non symbol is OK too'); 376 | }()); 377 | } 378 | 379 | (function () { 380 | var args = [{a:[1]}, null, ' ']; 381 | console.assert(Flatted.stringify.apply(null, args) === "[{\n \"a\": \"1\"\n},[\n 1\n]]", 'extra args same as JSON'); 382 | }()); 383 | 384 | (function () { 385 | var o = {a: 1, b: {a: 1, b: 2}}; 386 | var json = JSON.stringify(o, ['b']); 387 | console.assert( 388 | Flatted.stringify(o, ['b']) === '[{"b":"1"},{"b":2}]', 389 | 'whitelisted ["b"]: '+ json 390 | ); 391 | }()); 392 | 393 | (function () { 394 | var a = { b: { '': { c: { d: 1 } } } }; 395 | a._circular = a.b['']; 396 | var json = Flatted.stringify(a); 397 | var nosj = Flatted.parse(json); 398 | console.assert( 399 | nosj._circular === nosj.b[''] && 400 | JSON.stringify(nosj._circular) === JSON.stringify(a._circular), 401 | 'empty keys as non root objects work' 402 | ); 403 | delete a._circular; 404 | delete nosj._circular; 405 | console.assert( 406 | JSON.stringify(nosj) === JSON.stringify(a), 407 | 'objects copied with circular empty keys are the same' 408 | ); 409 | }()); 410 | 411 | ['65515.json', '65518.json'].forEach(fileName => { 412 | let dataString = require('fs').readFileSync('test/' + fileName).toString('utf-8'); 413 | let rawJson = JSON.parse(dataString); 414 | let {toolData} = rawJson; 415 | console.assert(typeof Flatted.parse(JSON.stringify(toolData)) === 'object'); 416 | }); 417 | 418 | class RecursiveMap extends Map { 419 | static fromJSON(any) { 420 | return new this(Flatted.fromJSON(any)); 421 | } 422 | toJSON() { 423 | return Flatted.toJSON([...this.entries()]); 424 | } 425 | } 426 | 427 | const jsonMap = new RecursiveMap([['test', 'value']]); 428 | const asJSON = JSON.stringify(jsonMap); 429 | const expected = '[["1"],["2","3"],"test","value"]'; 430 | console.assert(asJSON === expected, 'toJSON'); 431 | const revived = RecursiveMap.fromJSON(JSON.parse(asJSON)); 432 | console.assert(revived.get('test') === 'value', 'fromJSON'); 433 | 434 | 435 | const recursive = new RecursiveMap; 436 | const same = {}; 437 | same.same = same; 438 | recursive.set('same', same); 439 | 440 | const asString = JSON.stringify(recursive); 441 | const asMap = RecursiveMap.fromJSON(JSON.parse(asString)); 442 | console.assert(asMap.get('same').same === asMap.get('same'), 'RecursiveMap'); 443 | -------------------------------------------------------------------------------- /test/data.json: -------------------------------------------------------------------------------- 1 | [{"id":1,"sex":"Female","age":38,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"1st 2nd 3rd or 4th grade","race":"White"},{"id":2,"sex":"Female","age":44,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Married-civilian spouse present","education":"Associates degree-occup /vocational","race":"White"},{"id":3,"sex":"Male","age":2,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":4,"sex":"Female","age":35,"classOfWorker":"Private","maritalStatus":"Divorced","education":"High school graduate","race":"White"},{"id":5,"sex":"Male","age":49,"classOfWorker":"Private","maritalStatus":"Divorced","education":"High school graduate","race":"White"},{"id":6,"sex":"Male","age":13,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":7,"sex":"Female","age":1,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":8,"sex":"Female","age":61,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":9,"sex":"Male","age":38,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Masters degree(MA MS MEng MEd MSW MBA)","race":"Black"},{"id":10,"sex":"Female","age":7,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":11,"sex":"Female","age":30,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":12,"sex":"Male","age":85,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"10th grade","race":"White"},{"id":13,"sex":"Female","age":33,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"Asian or Pacific Islander"},{"id":14,"sex":"Male","age":26,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"White"},{"id":15,"sex":"Female","age":46,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":16,"sex":"Female","age":19,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"White"},{"id":17,"sex":"Male","age":11,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":18,"sex":"Male","age":23,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":19,"sex":"Male","age":27,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":20,"sex":"Male","age":35,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Divorced","education":"High school graduate","race":"Black"},{"id":21,"sex":"Male","age":8,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":22,"sex":"Female","age":29,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":23,"sex":"Female","age":40,"classOfWorker":"Private","maritalStatus":"Divorced","education":"Some college but no degree","race":"White"},{"id":24,"sex":"Male","age":24,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":25,"sex":"Male","age":45,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Some college but no degree","race":"White"},{"id":26,"sex":"Female","age":27,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"White"},{"id":27,"sex":"Male","age":41,"classOfWorker":"Local government","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":28,"sex":"Female","age":14,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":29,"sex":"Male","age":73,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"7th and 8th grade","race":"White"},{"id":30,"sex":"Male","age":46,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Some college but no degree","race":"White"},{"id":31,"sex":"Female","age":78,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"7th and 8th grade","race":"White"},{"id":32,"sex":"Male","age":27,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":33,"sex":"Female","age":81,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"9th grade","race":"White"},{"id":34,"sex":"Male","age":35,"classOfWorker":"Private","maritalStatus":"Never married","education":"Masters degree(MA MS MEng MEd MSW MBA)","race":"White"},{"id":35,"sex":"Female","age":15,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"7th and 8th grade","race":"White"},{"id":36,"sex":"Female","age":27,"classOfWorker":"State government","maritalStatus":"Never married","education":"12th grade no diploma","race":"Black"},{"id":37,"sex":"Male","age":68,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"Less than 1st grade","race":"Other"},{"id":38,"sex":"Female","age":28,"classOfWorker":"Private","maritalStatus":"Never married","education":"11th grade","race":"Black"},{"id":39,"sex":"Male","age":54,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Masters degree(MA MS MEng MEd MSW MBA)","race":"White"},{"id":40,"sex":"Female","age":37,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":41,"sex":"Male","age":82,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"7th and 8th grade","race":"White"},{"id":42,"sex":"Female","age":55,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"Asian or Pacific Islander"},{"id":43,"sex":"Male","age":77,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"Less than 1st grade","race":"White"},{"id":44,"sex":"Male","age":53,"classOfWorker":"Private","maritalStatus":"Divorced","education":"Some college but no degree","race":"White"},{"id":45,"sex":"Male","age":25,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"White"},{"id":46,"sex":"Male","age":23,"classOfWorker":"Private","maritalStatus":"Never married","education":"Associates degree-academic program","race":"White"},{"id":47,"sex":"Female","age":0,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":48,"sex":"Female","age":49,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Some college but no degree","race":"White"},{"id":49,"sex":"Female","age":75,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"10th grade","race":"White"},{"id":50,"sex":"Male","age":80,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"High school graduate","race":"White"},{"id":51,"sex":"Female","age":10,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"Asian or Pacific Islander"},{"id":52,"sex":"Male","age":22,"classOfWorker":"State government","maritalStatus":"Never married","education":"Associates degree-occup /vocational","race":"White"},{"id":53,"sex":"Female","age":61,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"9th grade","race":"White"},{"id":54,"sex":"Female","age":1,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":55,"sex":"Female","age":43,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":56,"sex":"Male","age":48,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":57,"sex":"Female","age":5,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"Black"},{"id":58,"sex":"Female","age":16,"classOfWorker":"Never worked","maritalStatus":"Never married","education":"10th grade","race":"White"},{"id":59,"sex":"Female","age":27,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"High school graduate","race":"White"},{"id":60,"sex":"Male","age":61,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":61,"sex":"Female","age":18,"classOfWorker":"Private","maritalStatus":"Never married","education":"11th grade","race":"Asian or Pacific Islander"},{"id":62,"sex":"Male","age":54,"classOfWorker":"Local government","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":63,"sex":"Male","age":50,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"7th and 8th grade","race":"White"},{"id":64,"sex":"Female","age":64,"classOfWorker":"Private","maritalStatus":"Widowed","education":"High school graduate","race":"White"},{"id":65,"sex":"Male","age":64,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"1st 2nd 3rd or 4th grade","race":"Black"},{"id":66,"sex":"Male","age":3,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":67,"sex":"Female","age":45,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"11th grade","race":"White"},{"id":68,"sex":"Female","age":72,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":69,"sex":"Male","age":80,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":70,"sex":"Male","age":47,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"Black"},{"id":71,"sex":"Female","age":39,"classOfWorker":"Private","maritalStatus":"Divorced","education":"Associates degree-occup /vocational","race":"White"},{"id":72,"sex":"Male","age":51,"classOfWorker":"Self-employed-not incorporated","maritalStatus":"Married-civilian spouse present","education":"Some college but no degree","race":"White"},{"id":73,"sex":"Male","age":12,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"Asian or Pacific Islander"},{"id":74,"sex":"Male","age":41,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"Asian or Pacific Islander"},{"id":75,"sex":"Male","age":39,"classOfWorker":"Local government","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":76,"sex":"Female","age":67,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":77,"sex":"Female","age":59,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":78,"sex":"Female","age":48,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"10th grade","race":"White"},{"id":79,"sex":"Female","age":42,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"7th and 8th grade","race":"White"},{"id":80,"sex":"Male","age":38,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Some college but no degree","race":"White"},{"id":81,"sex":"Female","age":28,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Masters degree(MA MS MEng MEd MSW MBA)","race":"White"},{"id":82,"sex":"Male","age":8,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":83,"sex":"Female","age":4,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":84,"sex":"Male","age":26,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"White"},{"id":85,"sex":"Female","age":55,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"Associates degree-occup /vocational","race":"White"},{"id":86,"sex":"Female","age":42,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":87,"sex":"Male","age":18,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"12th grade no diploma","race":"White"},{"id":88,"sex":"Female","age":14,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":89,"sex":"Male","age":4,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":90,"sex":"Female","age":32,"classOfWorker":"Private","maritalStatus":"Never married","education":"Some college but no degree","race":"Black"},{"id":91,"sex":"Female","age":83,"classOfWorker":"Not in universe","maritalStatus":"Widowed","education":"7th and 8th grade","race":"White"},{"id":92,"sex":"Female","age":5,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":93,"sex":"Female","age":33,"classOfWorker":"Not in universe","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":94,"sex":"Female","age":57,"classOfWorker":"Self-employed-incorporated","maritalStatus":"Widowed","education":"12th grade no diploma","race":"White"},{"id":95,"sex":"Female","age":6,"classOfWorker":"Not in universe","maritalStatus":"Never married","education":"Children","race":"White"},{"id":96,"sex":"Female","age":37,"classOfWorker":"Local government","maritalStatus":"Married-civilian spouse present","education":"Bachelors degree(BA AB BS)","race":"White"},{"id":97,"sex":"Female","age":47,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":98,"sex":"Female","age":54,"classOfWorker":"Not in universe","maritalStatus":"Divorced","education":"9th grade","race":"White"},{"id":99,"sex":"Female","age":55,"classOfWorker":"Private","maritalStatus":"Married-civilian spouse present","education":"High school graduate","race":"White"},{"id":100,"sex":"Female","age":44,"classOfWorker":"Private","maritalStatus":"Divorced","education":"Some college but no degree","race":"White"}] --------------------------------------------------------------------------------