|
1 | | -# jsonpath |
2 | | -JSONPath in the vein of S. Goessner, with some syntax refinements, eforcement, and extensions. |
| 1 | +# brunerd JSONPath |
| 2 | + |
| 3 | +Another take on the [JSONPath](https://goessner.net/articles/JsonPath/) query language by Stefan Goessner. |
| 4 | +The engine is purposefully written in ES5 for the broadest compatibility. |
| 5 | +The normalize engine has been reworked to parse a path expression into an array. Previously the path was expressed internally as a semi-colon delimited string, which meant keys with semi-colons would fail and also allowed for many invalid expressions to slip by. |
| 6 | + |
| 7 | +Notable enhancements include: |
| 8 | +- Arrays can now be referenced with positive *and* negative integers |
| 9 | +- Slice now allows a negative step integer, script expressions and can now operate on arrays *and* strings |
| 10 | +- Property names can be referenced using Unicode `\u` escape sequences |
| 11 | +- Property names containing `;` and `]` are no longer inaccessible, no more gotchas |
| 12 | +- Path output for JSONPath with options for dot style property names and single or double quote bracket styles |
| 13 | +- Path output in RFC6901 JSON Pointer style |
| 14 | +- Unions can now contain any and all valid JSONPath expressions as well as mixed quoting styles |
| 15 | + |
| 16 | +## JSONPath Syntax |
| 17 | + |
| 18 | +JSONPath Expression | Description |
| 19 | +-|- |
| 20 | +`$` | The root of the object, all queries must begin with this |
| 21 | +`.key`| Child operator `.` references the property named `key` (property names observe [Javascript naming rules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors)) |
| 22 | +`..key`| Recursive descent operator `..` references all properties name `key` within the object |
| 23 | +`.*` or `[*]`| Wildcard operator `*` matches all object property name or array indices |
| 24 | +`..*` or `..[*]`| Wildcard operator with recursive descent explodes the contents of your JSON quite well |
| 25 | +`()`| A script expression uses the returned value of the expression to as the property name or index |
| 26 | +`?()`| A filter expression interrogates all array/object members against the expression, descending into or returning the value of those that match |
| 27 | +`@` | Use inside a filter or script expression. `@` is substituted with the value of the current object, `@name` will match the current property name, `@.length` will reference the length of an array or string, `@.key` will reference the property "key" of the current object |
| 28 | +`[]`| Subscript/child operator; can contain quoted property names (`'key'`,`"key"`), numbers (negative or positive), filter and script expressions, `*` and `-` operators |
| 29 | +`[start:end:step]`| Array/string slice operator like Python's, all field are optional, start and end default to bounds, step can be negative |
| 30 | +`[,]`| Union operator `,` allows multiple quoted key names, array indices, slices, script/filter expressions, and `*` to be combined |
| 31 | +`[-]`| One _after_ the last element in an array, borrowed from JSON Pointer, used for JSON creation only (not retrieval) |
| 32 | + |
| 33 | +## Example queries: |
| 34 | +Use with the sample store.json data found below... |
| 35 | + |
| 36 | +JSONPath Query| Description |
| 37 | +-|- |
| 38 | +`$.*` | Contents of all top level elements, in this case "store" and "expensive" |
| 39 | +`$..*` | All members of the JSON structure, mercilessly exploded via recursive descent |
| 40 | +`$.store.*` | All things in store, which are some books and a red bicycle |
| 41 | +`$.store..price` | The price of everything in the store |
| 42 | +`$..author` | All authors using recursive descent and dot notation |
| 43 | +`$..['author']` | All authors using recursive descent and single quoted bracket notation |
| 44 | +`$..["author"]` | All authors using recursive descent and double quoted bracket notation |
| 45 | +`$.store.book[*].author` | All authors of all books in the store (ensures "bicycle" is not included) |
| 46 | +`$.store.book[*]["author","title"]` | All authors and titles of all books within an array via union |
| 47 | +`$.expensive` | What is considered expensive in this store? |
| 48 | +`$..book[?(@.price <= $["expensive"])]` | All books less than or equal to the 'expensive' property |
| 49 | +`$..book[?(@.price > $.expensive)]` | All books more than the 'expensive' property |
| 50 | +`$["store"]..price` | The price of everything in the store |
| 51 | +`$..book[?(@.comment)]` | Filter all books with a "comment" property containing data |
| 52 | +`$..book[?(@.comment !== undefined)]` | Filter all books with a "comment" property |
| 53 | +`$.store.book[?(@.comment_hidden)]` | Filter books with the "comment_hidden" property |
| 54 | +`$..book[2]` | The third book (zero based array) |
| 55 | +`$..book[-1]` | The last book via negative index |
| 56 | +`$..book[(@.length-1)]`| The last book via script expression subscript |
| 57 | +`$..book[(@.length/2)]`| The middle book via script expression subscript |
| 58 | +`$..book[-2]` | The next to last book only via negative index |
| 59 | +`$..book[-2:]` | The last two books via slice |
| 60 | +`$..book[::-1]` | All books in the array in reverse order via slice |
| 61 | +`$..book[:(@.length/2)]` | First half of books |
| 62 | +`$..book[(@.length/2):]` | Last half of the books |
| 63 | +`$..book[0,1]`| The first two books via subscript union |
| 64 | +`$..book[:2]` | The first two books via subscript array slice |
| 65 | +`$..book[?(@.isbn)]` | Filter all books with isbn number |
| 66 | +`$..book[?(@.price<10)]`| Filter all books less than 10 |
| 67 | +`$..book[?(!(@.price<10))]`| Filter all books NOT less than 10 |
| 68 | +`$..book[?(@.price>=10)]`| Filter all books greater than or equal to 10 (same as above) |
| 69 | +`$..book[?(@.price==8.95)]`| Filter all books that cost exactly 8.95 |
| 70 | +`$..book[?(@.category=="fiction")]` | Filter all books with a category of "fiction" exactly (will not match "Fiction") |
| 71 | +`$..book[?(@.price < $.expensive && @.category=~/fiction/i)]` | Filter all books less than the root property of "expensive" with case-insensitive category of "fiction" |
| 72 | +`$..book[?(@.price < $.expensive && /fiction/i.test(@.category))]` | Same as above but with Javascript style regex used in the filter expression, supported but ugly |
| 73 | +`$..book[?(@.price < 10 \|\| @.category=~/humor/i)]` | Filter all books less than 10 OR with case-insensitive category containing "humor" |
| 74 | +`$..book[?(@.title =~/\u9053\u5fb7/)]` | Finds all books beginning with 道德 using Unicode escape sequences in a regex |
| 75 | +`$.store.emoji["\ud83e\udd13"]` | Look up a Unicode key name using Unicode escape sequences |
| 76 | + |
| 77 | +### Sample data: |
| 78 | +Adapted and expanded from Stefan Goessner's [original post](http://goessner.net/articles/JsonPath/) |
| 79 | +<details><summary><b>store.json</b></summary> |
| 80 | +<p> |
| 81 | + |
| 82 | +```javascript |
| 83 | +{ |
| 84 | + "store": { |
| 85 | + "book": [ |
| 86 | + { |
| 87 | + "category": "reference", |
| 88 | + "author": "Nigel Rees", |
| 89 | + "title": "Sayings of the Century", |
| 90 | + "price": 8.95, |
| 91 | + "comment":"", |
| 92 | + "comment_hidden": "\"A bird in hand is worth two in the bush\" is still an awkward phrase." |
| 93 | + }, |
| 94 | + { |
| 95 | + "category": "reference/humor", |
| 96 | + "author": "Eric S. Raymond", |
| 97 | + "title": "New Hackers Dictionary, 3rd edition", |
| 98 | + "isbn":"0-262-68092-0", |
| 99 | + "price": 18.99, |
| 100 | + "comment":"If you are a \ud83e\udd13 you are sure to be amused" |
| 101 | + }, |
| 102 | + { |
| 103 | + "category": "fiction", |
| 104 | + "author": "Evelyn Waugh", |
| 105 | + "title": "Sword of Honour", |
| 106 | + "price": 12.99, |
| 107 | + "comment":"Page after page of war stories. Fun.", |
| 108 | + "comment_hidden":"This book is likely cursed." |
| 109 | + }, |
| 110 | + { |
| 111 | + "category": "Fiction", |
| 112 | + "author": "Herman Melville", |
| 113 | + "title": "Moby Dick", |
| 114 | + "isbn": "0-553-21311-3", |
| 115 | + "price": 8.99, |
| 116 | + "comment":"Before Jaws, there was Moby Dick", |
| 117 | + "comment_hidden":"Based on Mocha Dick, who received no royalties" |
| 118 | + }, |
| 119 | + { |
| 120 | + "category": "fiction", |
| 121 | + "author": "J. R. R. Tolkien", |
| 122 | + "title": "The Lord of the Rings", |
| 123 | + "isbn": "0-395-19395-8", |
| 124 | + "price": 22.99, |
| 125 | + "comment":"Precious. My precious. Collectors edition.", |
| 126 | + "comment_hidden":"Cursed but totally worth it." |
| 127 | + }, |
| 128 | + { |
| 129 | + "category":"philosophy", |
| 130 | + "author":"老子", |
| 131 | + "title":"道德经", |
| 132 | + "price":30, |
| 133 | + "comment":"The Tao Te Ching in Chinese" |
| 134 | + }, |
| 135 | + { |
| 136 | + "category":"philosophy", |
| 137 | + "author":"老子", |
| 138 | + "title":"道德經", |
| 139 | + "price":80, |
| 140 | + "comment":"The Tao Te Ching in Chinese, original title" |
| 141 | + } |
| 142 | + ], |
| 143 | + "bicycle": { |
| 144 | + "author":"Bikes don't have authors, silly", |
| 145 | + "color": "red", |
| 146 | + "price": 19.95, |
| 147 | + "comment":"A great bike for a kid!", |
| 148 | + "comment_hidden":"Cursed but shiny!" |
| 149 | + }, |
| 150 | + "emoji":{ |
| 151 | + "🤓":{ |
| 152 | + "description":"smiling face with glasses", |
| 153 | + "description_alternate":"nerd" |
| 154 | + } |
| 155 | + } |
| 156 | + }, |
| 157 | + "expensive": 20 |
| 158 | +} |
| 159 | +``` |
| 160 | +</details> |
| 161 | + |
| 162 | +## Source Files |
| 163 | + |
| 164 | +- [jsonpath.js](./jsonpath.js) The JSONPath engine. |
| 165 | +- [jsonpath.no_comment.js](./jsonpath.no_comment.js) Same as above but without comments. |
| 166 | +- [jsonpath.min.js](./jsonpath.min.js) The minified "one-liner" version (not obfuscated) |
| 167 | + |
| 168 | +### Invoking the `jsonpath()` function: |
| 169 | +`jsonPath(obj, expr [, arg])` |
| 170 | + |
| 171 | +Parameters: |
| 172 | +`obj` (Object|Array|String|Number|Boolean|null): |
| 173 | + Object representing the JSON structure |
| 174 | + |
| 175 | +`expr` (String|Array): |
| 176 | + Either a JSONPath expression in string form or a pre-composed array representation of a JSONPath or JSON Pointer expression. |
| 177 | + |
| 178 | +`arg` (Object|undefined): |
| 179 | + The `arg` object controls output, it can contain the following properties and values: |
| 180 | + |
| 181 | +`resultType` value | Description |
| 182 | +-|- |
| 183 | +`VALUE`|the result is the matching values (default) |
| 184 | +`PATH`|path(s) matched by the query in bracket notation with double quotes |
| 185 | +`PATH_DOTTED`|path(s) matched by the query in dot notation where possible with double quoted bracket notation otherwise |
| 186 | +`PATH_JSONPOINTER`|path(s) matched by the query in [JSON Pointer (RFC6901)](https://tools.ietf.org/html/rfc6901) format |
| 187 | + |
| 188 | +Properties that apply to `PATH` and `PATH_DOTTED` `resultType` output: |
| 189 | + |
| 190 | +`singleQuoteKeys` value | Description |
| 191 | +-|- |
| 192 | +`true`|Use single quotes for bracket notation |
| 193 | +`false`|The default, uses double quotes for bracket notation |
| 194 | + |
| 195 | +`escapeUnicode` value| Description |
| 196 | +-|- |
| 197 | +`true` | Uses Unicode `\u` escape sequences for all characters outside the Basic Latin Unicode block |
| 198 | +`false` | The default, characters `\u0000-\u001f` are *always* Unicode escaped, with exceptions of `\b` `\f` `\n` `\r` and `\t` |
| 199 | + |
| 200 | +Example `arg` object: |
| 201 | +`{resultType:"PATH_DOTTED",singleQuoteKeys:true,escapeUnicode:true}` |
| 202 | + |
| 203 | +Return value: |
| 204 | +Always an array. Empty or otherwise. The original implementation returns false for no matches. |
| 205 | + |
| 206 | +### JSONPath `expr` internal representation: |
| 207 | +Within `jsonpath()` the `expr` string is parsed into an array containing strings, numbers, and objects (with a single property of "expression") |
| 208 | + |
| 209 | +JSONPath example: |
| 210 | +`$[*][(@.length/2)]["key"][?(@.subKey == 'cool')][0]` this expression is converted internally to an array: |
| 211 | +`[{"expression":"*"},{"expression":"(@.length/2)"},"key",{"expression":"?(@.subKey == \"cool\")"},0]` |
| 212 | + |
| 213 | +An array of this same format can be passed into `jsonpath()` directly. |
| 214 | +Since JSON Pointer is so easily parsed, this allows for jsonpath() to be given an `expr` array derived from JSON Pointer. |
| 215 | + |
| 216 | +```javascript |
| 217 | +//split on / |
| 218 | +expr = expr.split('/') |
| 219 | +//throw out first entry |
| 220 | +expr.shift() |
| 221 | +//replace special symbols ~1 and ~0 (in this order) with the actual characters |
| 222 | +//convert string representations of numbers to Number types (for proper quoting in PATH output) |
| 223 | +expr = expr.map(function (f){ |
| 224 | + return f.replace(/~1/g,"/").replace(/~0/g,"~") }) |
| 225 | + .map(function(a){ return a === "" ? "" : isNaN(a) ? a : Number(a)}) |
| 226 | +``` |
| 227 | + |
| 228 | +JSON Pointer example: |
| 229 | +`/0/key/sub/` is a JSON Pointer the JSONPath equivalent is: `$[0].key.sub[""]` (yes, property names with empty strings are allowed in JSON!) |
| 230 | +Using the above code `expr` will convert internally to: `[0,"key","sub",""]` |
| 231 | + |
| 232 | +## brunerd JSONPath grammar |
| 233 | +Hat tip to [cburgmer](https://github.com/cburgmer) for providing the bones of the grammar declaration with [Proposal A](https://github.com/cburgmer/json-path-comparison/blob/master/proposals/Proposal_A/README.md) which I've adjusted for this implementation |
| 234 | + |
| 235 | + Start |
| 236 | + ::= "$" Operator* |
| 237 | + |
| 238 | + Operator |
| 239 | + ::= DotChild |
| 240 | + | BracketChildren |
| 241 | + | RecursiveDescentChildren |
| 242 | + |
| 243 | + DotChild |
| 244 | + ::= "." DotChildName |
| 245 | + | ".*" |
| 246 | + |
| 247 | + DotChildName |
| 248 | + ::= [^0-9 -#%-\/:-@\[-^`{-~][\w\d$]* |
| 249 | + |
| 250 | + BracketChildren |
| 251 | + ::= "[" ws BracketElements ws "]" |
| 252 | + |
| 253 | + BracketElements |
| 254 | + ::= BracketElement ws "," ws BracketElements |
| 255 | + | BracketElement |
| 256 | + |
| 257 | + BracketElement |
| 258 | + ::= Integer? ":" Integer? ":" NonZeroInteger? |
| 259 | + | Integer? ":" Integer? |
| 260 | + | BracketChild |
| 261 | + | "*" |
| 262 | + | "-" |
| 263 | + | "?(" FilterExpression ")" |
| 264 | + | "(" ScriptExpression ")" |
| 265 | + |
| 266 | + RecursiveDescentChildren |
| 267 | + ::= ".." DotChildName |
| 268 | + | "..*" |
| 269 | + | ".." BracketChildren |
| 270 | + |
| 271 | + BracketChild |
| 272 | + ::= "'" SingleQuotedString "'" |
| 273 | + | '"' DoubleQuotedString '"' |
| 274 | + | Integer |
| 275 | + |
| 276 | + FilterExpression |
| 277 | + ::= LogicalAnd |
| 278 | + | LogicalOr |
| 279 | + | HigherPrecedenceFilterExpression |
| 280 | + |
| 281 | + ScriptExpression |
| 282 | + ::= LogicalAnd |
| 283 | + | LogicalOr |
| 284 | + | HigherPrecedenceFilterExpression |
| 285 | + |
| 286 | + LogicalAnd |
| 287 | + ::= HigherPrecedenceFilterExpression ws "&&" ws LogicalAnd |
| 288 | + | HigherPrecedenceFilterExpression ws "&&" ws HigherPrecedenceFilterExpression |
| 289 | + |
| 290 | + LogicalOr |
| 291 | + ::= HigherPrecedenceFilterExpression ws "||" ws LogicalOr |
| 292 | + | HigherPrecedenceFilterExpression ws "||" ws HigherPrecedenceFilterExpression |
| 293 | + |
| 294 | + HigherPrecedenceFilterExpression |
| 295 | + ::= FilterValue ws ComparisonOperator ws FilterValue |
| 296 | + | UnaryFilterExpression |
| 297 | + |
| 298 | + UnaryFilterExpression |
| 299 | + ::= FilterValue |
| 300 | + | "!" ws UnaryFilterExpression |
| 301 | + | "(" ws FilterExpression ws ")" |
| 302 | + |
| 303 | + ComparisonOperator |
| 304 | + ::= "==" |
| 305 | + | "!=" |
| 306 | + | "===" |
| 307 | + | "!==" |
| 308 | + | "=~" |
| 309 | + | "<=" |
| 310 | + | ">=" |
| 311 | + | "<" |
| 312 | + | ">" |
| 313 | + |
| 314 | + FilterValue |
| 315 | + ::= "@" ScalarOperator* |
| 316 | + | "$" ScalarOperator* |
| 317 | + | SimpleValue |
| 318 | + |
| 319 | + ScalarOperator |
| 320 | + ::= "." DotChildName |
| 321 | + | "[" ws BracketChild ws "]" |
| 322 | + |
| 323 | + SimpleValue |
| 324 | + ::= "'" SingleQuotedString "'" |
| 325 | + | '"' DoubleQuotedString '"' |
| 326 | + | '/' RegexSearch '/' |
| 327 | + | Number |
| 328 | + | "false" |
| 329 | + | "true" |
| 330 | + | "null" |
| 331 | + | undefined |
| 332 | + |
| 333 | +## References |
| 334 | +[Stefan Gössner](https://goessner.net/articles/JsonPath/) and his initial work on the concept of JSONPath. |
| 335 | + |
| 336 | +Christoph Burgmer's [Proposal A](https://github.com/cburgmer/json-path-comparison/blob/master/proposals/Proposal_A/README.md) and his [JSON Path comparison](https://cburgmer.github.io/json-path-comparison/) matrix |
| 337 | +A vast collection of open source JSONPath implementations with all their their varying behaviors. |
| 338 | + |
0 commit comments