├── .gitignore ├── LICENSE ├── README.md ├── bin.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Anton Medvedev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-json 2 | 3 | This package implements fast extraction of part of JSON. 4 | 5 | * 3.5x times faster than jq 6 | * 4.5x times faster than JSON.parse 7 | 8 | Notes: 9 | 10 | * Fastest way of extracting part of JSON from file 11 | * Arrays not supported currently 12 | * Does not check for valid JSON (you can grab "foo" from here: `{"foo": "bar", here goes anything`) 13 | 14 | ## Install 15 | 16 | ```bash 17 | npm i @medv/fast-json 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```js 23 | const fastJSON = require('@medv/fast-json') 24 | 25 | const result = fastJSON(input, path) 26 | ``` 27 | 28 | Another example: 29 | 30 | ```js 31 | const result = fastJSON('{"foo": {"bar": 1}}', ['foo', 'bar']) 32 | ``` 33 | 34 | ## CLI 35 | 36 | ```bash 37 | npm i -g @medv/fast-json 38 | ``` 39 | 40 | ```bash 41 | cat data.json | fast-json path to field 42 | ``` 43 | 44 | ## Benchmarks 45 | 46 | Benchmarks were made with [hyperfine](https://github.com/sharkdp/hyperfine) on a big json (around 400mb). 47 | 48 | ``` 49 | Benchmark #1: cat data.json | fast-json gates aeroflot_ndc_gate gates_info airline_iatas 50 | 51 | Time (mean ± σ): 4.080 s ± 0.181 s [User: 3.206 s, System: 1.205 s] 52 | 53 | Range (min … max): 3.877 s … 4.292 s 54 | 55 | Benchmark #2: cat data.json | jq .gates.aeroflot_ndc_gate.gates_info.airline_iatas 56 | 57 | Time (mean ± σ): 14.938 s ± 0.198 s [User: 13.009 s, System: 2.170 s] 58 | 59 | Range (min … max): 14.808 s … 15.347 s 60 | 61 | Benchmark #3: cat data.json | fx .gates.aeroflot_ndc_gate.gates_info.airline_iatas 62 | 63 | Time (mean ± σ): 18.443 s ± 0.356 s [User: 17.495 s, System: 2.661 s] 64 | 65 | Range (min … max): 17.731 s … 19.179 s 66 | 67 | ``` 68 | 69 | # License 70 | 71 | MIT 72 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --max-old-space-size=8192 2 | 'use strict' 3 | const fastJSON = require('.') 4 | 5 | const usage = ` 6 | Usage 7 | $ fast-json [path ...] 8 | 9 | Examples 10 | $ cat data.json | fast-json london geo point 11 | {...} 12 | 13 | ` 14 | 15 | function main(input) { 16 | if (input === '') { 17 | console.log(usage) 18 | process.exit(2) 19 | } 20 | 21 | const path = process.argv.slice(2) 22 | const result = fastJSON(input, path) 23 | 24 | if (typeof result === 'undefined') { 25 | process.stderr.write('undefined\n') 26 | process.exit(1) 27 | } else if (typeof result === 'string') { 28 | console.log(result) 29 | } else { 30 | console.log(JSON.stringify(result, null, 2)) 31 | } 32 | } 33 | 34 | const stdin = process.stdin 35 | let buff = '' 36 | 37 | if (stdin.isTTY) { 38 | main(buff) 39 | } 40 | 41 | stdin.setEncoding('utf8') 42 | 43 | stdin.on('readable', () => { 44 | let chunk 45 | 46 | while ((chunk = stdin.read())) { 47 | buff += chunk 48 | } 49 | }) 50 | 51 | stdin.on('end', () => { 52 | main(buff) 53 | }) 54 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fast extract part of json by path. 3 | * Example: fastJSON('{...}', ['foo', 'bar']) 4 | * 5 | * @param {String} input JSON 6 | * @param {Array} path Path for extraction 7 | * @returns {*} 8 | * @throws SyntaxError 9 | */ 10 | function fastJSON(input, path) { 11 | let lookup = path.shift() // Holds key what we should find next 12 | 13 | let record = false // Triggers setting of next variables. 14 | let start = 0, end = 0 // Holds found offsets in input. 15 | 16 | const stack = [] // Count brackets and ":" sign. 17 | const isKeys = 0, isArray = 1, isValue = 2 18 | let level = 0 // Current depth level. 19 | let on = 1 // What level we are expecting right now. 20 | 21 | loop: for (let i = 0, len = input.length; i < len; i++) { 22 | const ch = input[i] 23 | switch (ch) { 24 | 25 | case '{': { 26 | stack.push(isKeys) 27 | level++ 28 | break 29 | } 30 | 31 | case '}': { 32 | const t = stack.pop() 33 | if (t !== isValue && t !== isKeys) { 34 | throw new SyntaxError(`Unexpected token ${ch} in JSON at position ${i}`) 35 | } 36 | level-- 37 | if (record && level < on) { 38 | end = i - 1 39 | break loop 40 | } 41 | break 42 | } 43 | 44 | case '[': { 45 | stack.push(isArray) 46 | level++ 47 | break 48 | } 49 | 50 | case ']': { 51 | if (stack.pop() !== isArray) { 52 | throw new SyntaxError(`Unexpected token ${ch} in JSON at position ${i}`) 53 | } 54 | level-- 55 | if (record && level < on) { 56 | end = i - 1 57 | break loop 58 | } 59 | break 60 | } 61 | 62 | case ':': { 63 | const t = stack[stack.length - 1] 64 | if (t === isKeys) { 65 | stack[stack.length - 1] = isValue 66 | } 67 | if (record && level === on) { 68 | start = i + 1 69 | } 70 | break 71 | } 72 | 73 | case ',': { 74 | const t = stack[stack.length - 1] 75 | if (t === isValue) { 76 | stack[stack.length - 1] = isKeys 77 | } 78 | if (record && level === on) { 79 | end = i - 1 80 | break loop 81 | } 82 | break 83 | } 84 | 85 | case '"': 86 | let j = ++i // next char after " 87 | 88 | // Consume whole string till next " symbol. 89 | for (; j < len; j++) { 90 | const ch = input[j] 91 | 92 | if (ch === '"' && input[j - 1] !== '\\') { // Make sure " doesn't escaped. 93 | break 94 | } 95 | else if (ch == '"' && input[j - 1] === '\\') { // handle case with \\",\\\\" 96 | let backslashCount = 1; 97 | while (input[j - 1 - backslashCount] === '\\') { 98 | backslashCount++; 99 | } 100 | 101 | if (backslashCount % 2 === 0) { 102 | break; 103 | } 104 | } 105 | } 106 | 107 | // Check if current key is what we was looking for. 108 | const t = stack[stack.length - 1] 109 | if (t === isKeys && level === on && input.slice(i, j) === lookup) { 110 | if (path.length > 0) { 111 | lookup = path.shift() 112 | on++ 113 | } else { 114 | record = true 115 | } 116 | 117 | } 118 | 119 | i = j // Continue from end of string. 120 | break 121 | } 122 | } 123 | 124 | if (start !== 0 && start <= end) { 125 | const part = input.slice(start, end + 1) // We found it. 126 | return JSON.parse(part) 127 | } else if (level !== 0) { 128 | throw new SyntaxError(`JSON parse error`) 129 | } 130 | } 131 | 132 | module.exports = fastJSON 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@medv/fast-json", 3 | "version": "1.0.1", 4 | "description": "Extract part of JSON fastest way", 5 | "main": "index.js", 6 | "bin": "bin.js", 7 | "scripts": { 8 | "test": "ava" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/antonmedv/fast-json.git" 13 | }, 14 | "keywords": [ 15 | "json", 16 | "fast" 17 | ], 18 | "author": "Anton Medvedev ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/antonmedv/fast-json/issues" 22 | }, 23 | "homepage": "https://github.com/antonmedv/fast-json#readme", 24 | "devDependencies": { 25 | "ava": "^0.25.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const fastJSON = require('.') 3 | 4 | function get(json, ...path) { 5 | return fastJSON(JSON.stringify(json), path) 6 | } 7 | 8 | test(t => { 9 | const json = 'foo' 10 | t.is(get(json, 'foo'), void 0) 11 | }) 12 | 13 | test(t => { 14 | const json = 1 15 | t.is(get(json, 'foo'), void 0) 16 | }) 17 | 18 | test(t => { 19 | const json = [1, 2, 3] 20 | t.is(get(json, 'foo'), void 0) 21 | }) 22 | 23 | test(t => { 24 | const json = {foo: 'bar'} 25 | t.is(get(json, 'foo'), 'bar') 26 | t.is(get(json, 'bar'), void 0) 27 | }) 28 | 29 | test(t => { 30 | const r = fastJSON('{"foo": "bar", "baz": 1}', ['bar']) 31 | t.is(r, void 0) 32 | }) 33 | 34 | test(t => { 35 | const r = fastJSON('{"foo": "bar", here goes anything', ['foo']) 36 | t.is(r, 'bar') 37 | }) 38 | 39 | test(t => { 40 | const json = {foo: '"quote"'} 41 | t.is(get(json, 'foo'), '"quote"') 42 | }) 43 | 44 | test(t => { 45 | const json = {foo: {}} 46 | t.deepEqual(get(json, 'foo'), {}) 47 | }) 48 | 49 | test(t => { 50 | const json = {foo: 1} 51 | t.is(get(json, 'foo'), 1) 52 | }) 53 | 54 | test(t => { 55 | const json = {foo: 123} 56 | t.is(get(json, 'foo'), 123) 57 | }) 58 | 59 | test(t => { 60 | const json = {foo: 1, bar: 2} 61 | t.is(get(json, 'foo'), 1) 62 | t.is(get(json, 'bar'), 2) 63 | }) 64 | 65 | test(t => { 66 | const json = {foo: 'foo', bar: 'bar'} 67 | t.is(get(json, 'foo'), 'foo') 68 | t.is(get(json, 'bar'), 'bar') 69 | }) 70 | 71 | test(t => { 72 | const json = {foo: {bar: 'bar'}} 73 | t.deepEqual(get(json, 'foo'), json.foo) 74 | t.is(get(json, 'foo', 'bar'), 'bar') 75 | }) 76 | 77 | test(t => { 78 | const json = {foo: {bar: 'bar'}, baz: 1} 79 | t.deepEqual(get(json, 'foo'), json.foo) 80 | t.is(get(json, 'foo', 'bar'), 'bar') 81 | t.is(get(json, 'baz'), 1) 82 | }) 83 | 84 | test(t => { 85 | const json = {foo: {bar: 'bar'}, baz: 1} 86 | const r = fastJSON(JSON.stringify(json, null, 2), ['foo']) 87 | t.deepEqual(r, json.foo) 88 | }) 89 | 90 | test(t => { 91 | const json = {foo: {bar: 'bar'}, baz: 1} 92 | t.deepEqual(get(json, 'foo'), json.foo) 93 | t.is(get(json, 'foo', 'bar'), 'bar') 94 | t.is(get(json, 'baz'), 1) 95 | }) 96 | 97 | test(t => { 98 | const json = data() 99 | t.deepEqual(get(json, 'name'), json.name) 100 | t.deepEqual(get(json, 'friends'), json.friends) 101 | t.deepEqual(get(json, 'tags'), json.tags) 102 | t.deepEqual(get(json, 'range'), json.range) 103 | t.is(get(json, 'name', 'first'), json.name.first) 104 | t.is(get(json, 'age'), json.age) 105 | t.is(get(json, 'about'), json.about) 106 | }) 107 | 108 | test(t=>{ 109 | const json = {foo: "\\"} 110 | t.deepEqual(get(json, 'foo'), json.foo) 111 | }) 112 | 113 | test(t=>{ 114 | const json = {foo: "\\\\"} 115 | t.deepEqual(get(json, 'foo'), json.foo) 116 | }) 117 | 118 | test(t=>{ 119 | const json = {foo: "\\\\\\"} 120 | t.deepEqual(get(json, 'foo'), json.foo) 121 | }) 122 | 123 | test(t=>{ 124 | const json = {foo: "\\b"} 125 | t.deepEqual(get(json, 'foo'), json.foo) 126 | }) 127 | 128 | test(t=>{ 129 | const json = {foo: "b\\"} 130 | t.deepEqual(get(json, 'foo'), json.foo) 131 | }) 132 | 133 | test(t=>{ 134 | const json = {foo: "b\\"} 135 | t.deepEqual(get(json, 'foo'), json.foo) 136 | }) 137 | 138 | test(t=>{ 139 | const r = fastJSON('{"foo-1": "bar"}', ['foo-1']) 140 | t.is(r, 'bar') 141 | }) 142 | 143 | test(t=>{ 144 | const r = fastJSON('{"foo\\"": "bar"}', ['foo\\"']) /**This is wrong lookup, it should be foo" only, may need to fix later */ 145 | t.is(r, 'bar') 146 | }) 147 | 148 | test(t=>{ 149 | const r = fastJSON('{"foo\\\\": "bar"}', ['foo\\\\']) /**This is wrong lookup, it should be foo\\ only, may need to fix later */ 150 | t.is(r, 'bar') 151 | }) 152 | 153 | test(t=>{ 154 | const r = fastJSON('{"\\\\": "bar"}', ['\\\\']) /**This is wrong lookup, it should be \\ only, may need to fix later */ 155 | t.is(r, 'bar') 156 | }) 157 | 158 | 159 | 160 | function data() { 161 | return { 162 | "_id": "5b9cc0c64c0c3df825daf917", 163 | "index": 0, 164 | "guid": "63b2461a-4b87-4cc6-a7c5-fc4e797cffe5", 165 | "isActive": false, 166 | "balance": "$2,418.18", 167 | "picture": "http://placehold.it/32x32", 168 | "age": 27, 169 | "eyeColor": "brown", 170 | "name": { 171 | "first": "Felicia", 172 | "last": "Neal" 173 | }, 174 | "company": "TALKALOT", 175 | "email": "felicia.neal@talkalot.tv", 176 | "phone": "+1 (915) 582-3658", 177 | "address": "994 Grand Avenue, Bakersville, Nebraska, 3554", 178 | "about": "Dolor quis culpa aute amet elit aute labore eiusmod nostrud mollit. Dolor nostrud qui ex laboris Lorem ullamco nisi aliquip fugiat ipsum eiusmod reprehenderit elit ullamco. Minim occaecat aliquip excepteur reprehenderit tempor ea proident ad eu quis magna tempor. Nisi exercitation do et culpa excepteur magna reprehenderit enim duis dolor elit. Aute culpa enim occaecat fugiat deserunt. Nulla commodo veniam non elit adipisicing adipisicing adipisicing reprehenderit ex sit nostrud non.", 179 | "registered": "Wednesday, June 13, 2018 11:49 PM", 180 | "latitude": "3.388267", 181 | "longitude": "-95.363031", 182 | "tags": [ 183 | "nulla", 184 | "in", 185 | "ipsum", 186 | "deserunt", 187 | "do" 188 | ], 189 | "range": [ 190 | 0, 191 | 1, 192 | 2, 193 | 3, 194 | 4, 195 | 5, 196 | 6, 197 | 7, 198 | 8, 199 | 9 200 | ], 201 | "friends": [ 202 | { 203 | "id": 0, 204 | "name": "Kidd Hoover" 205 | }, 206 | { 207 | "id": 1, 208 | "name": "Bessie Norman" 209 | }, 210 | { 211 | "id": 2, 212 | "name": "Carr Evans" 213 | } 214 | ], 215 | "greeting": "Hello, Felicia! You have 8 unread messages.", 216 | "favoriteFruit": "strawberry" 217 | } 218 | } 219 | --------------------------------------------------------------------------------