json1-8 Exercise 2: 21 JSONPath Queries, Four Libraries, Hand-Written Answers ============================================================================== Run with Node 24.18.0 (json-p3 3.0.1, fast-json-patch 3.1.1, jsonpath-plus 11.2.0, json-merge-patch 1.0.2), Python 3.14.0 (jsonpointer 3.2.1, jsonpatch 1.35, python-jsonpath 2.2.1, jsonpath-ng 1.10.1, json-merge-patch 0.3.0) and jq 1.8.2, all installed in a scratch folder. Order: node ex2.mjs, then python ex2.py. jqeq.sh shows the jq equivalents. --- store.json (the bookstore document) --- { "store": { "book": [ { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 }, { "category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99 }, { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 }, { "category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "isbn": "0-395-19395-8", "price": 22.99 } ], "bicycle": { "color": "red", "price": 399 } } } --- queries.json (each query with the answer I wrote by hand BEFORE running anything) --- [ ["$.store.book[*].author", ["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"]], ["$..author", ["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"]], ["$.store.*", ["books[]","bicycle"]], ["$.store..price", [8.95,12.99,8.99,22.99,399]], ["$..book[2]", ["book:Moby Dick"]], ["$..book[-1:]", ["book:The Lord of the Rings"]], ["$..book[0,1]", ["book:Sayings of the Century","book:Sword of Honour"]], ["$..book[:2]", ["book:Sayings of the Century","book:Sword of Honour"]], ["$..book[?@.isbn]", ["book:Moby Dick","book:The Lord of the Rings"]], ["$..book[?@.price<10]", ["book:Sayings of the Century","book:Moby Dick"]], ["$..book[?(@.price<10)]", ["book:Sayings of the Century","book:Moby Dick"]], ["$..*", "27 values"], ["$.store.book[?@.category == \"fiction\" && @.price < 10].title", ["Moby Dick"]], ["$.store.book[?length(@.title) > 15].title", ["Sayings of the Century","The Lord of the Rings"]], ["$.store.book[?match(@.author, \"J.*\")].author", ["J. R. R. Tolkien"]], ["$.store.book[?search(@.title, \"Moby\")].title", ["Moby Dick"]], ["$.store.book[1:4:2].title", ["Sword of Honour","The Lord of the Rings"]], ["$.store.book[::-1].price", [22.99,8.99,12.99,8.95]], ["$['store']['bicycle']['color']", ["red"]], ["$.store.book[?@.nope == $.store.book[0].nope].price", [8.95,12.99,8.99,22.99]], ["$.store.book[5]", []] ] --- ex2.mjs --- import fs from 'node:fs'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const { query } = await import('json-p3'); const { JSONPath } = require('jsonpath-plus'); const doc = JSON.parse(fs.readFileSync('store.json', 'utf8')); const qs = JSON.parse(fs.readFileSync('queries.json', 'utf8')).map(x => x[0]); const key = v => Array.isArray(v) ? 'books[]' : v && typeof v === 'object' ? ('title' in v ? 'book:' + v.title : 'color' in v ? 'bicycle' : 'object') : v; const out = { 'json-p3': {}, 'jsonpath-plus': {} }; for (const q of qs) { try { const r = [...query(q, doc)].map(n => n.value); out['json-p3'][q] = q === '$..*' ? r.length + ' values' : r.map(key); } catch (e) { out['json-p3'][q] = 'ERROR'; } try { const r = JSONPath({ path: q, json: doc }); out['jsonpath-plus'][q] = q === '$..*' ? r.length + ' values' : r.map(key); } catch (e) { out['jsonpath-plus'][q] = 'ERROR'; } } fs.writeFileSync('jp_node.json', JSON.stringify(out)); console.log('Node results written for ' + qs.length + ' queries'); --- ex2.py --- import json, jsonpath from jsonpath_ng.ext import parse as ngparse doc = json.load(open('store.json', encoding='utf-8')) qe = json.load(open('queries.json', encoding='utf-8')) node = json.load(open('jp_node.json', encoding='utf-8')) def key(v): if isinstance(v, list): return 'books[]' if isinstance(v, dict): return 'book:' + v['title'] if 'title' in v else 'bicycle' if 'color' in v else 'object' return v def run(f, q): try: r = f(q); return (str(len(r)) + ' values') if q == '$..*' else [key(x) for x in r] except Exception: return 'ERROR' libs = { 'json-p3 3.0.1': lambda q: node['json-p3'][q], 'python-jsonpath 2.2.1': lambda q: run(lambda x: jsonpath.findall(x, doc), q), 'jsonpath-plus 11.2.0': lambda q: node['jsonpath-plus'][q], 'jsonpath-ng 1.10.1 (ext)': lambda q: run(lambda x: [m.value for m in ngparse(x).find(doc)], q), } score = {l: 0 for l in libs}; wrong = {l: [] for l in libs} for q, e in qe: for l, f in libs.items(): r = f(q) if r == e: score[l] += 1 else: wrong[l].append(q) for l in libs: print(l.ljust(26) + '%2d of %d queries give the hand-written expected answer' % (score[l], len(qe))) print() for l in libs: if wrong[l]: print(l + ' differs on:'); [print(' ' + w) for w in wrong[l]] # the two RFC 9535 implementations agree with each other everywhere? a, b = libs['json-p3 3.0.1'], libs['python-jsonpath 2.2.1'] print('\njson-p3 and python-jsonpath give identical answers on all queries:', all(a(q) == b(q) for q, _ in qe)) Output of node ex2.mjs: Node results written for 21 queries Output of python ex2.py: json-p3 3.0.1 21 of 21 queries give the hand-written expected answer python-jsonpath 2.2.1 21 of 21 queries give the hand-written expected answer jsonpath-plus 11.2.0 13 of 21 queries give the hand-written expected answer jsonpath-ng 1.10.1 (ext) 15 of 21 queries give the hand-written expected answer jsonpath-plus 11.2.0 differs on: $..book[?@.isbn] $..book[?@.price<10] $.store.book[?@.category == "fiction" && @.price < 10].title $.store.book[?length(@.title) > 15].title $.store.book[?match(@.author, "J.*")].author $.store.book[?search(@.title, "Moby")].title $.store.book[::-1].price $.store.book[?@.nope == $.store.book[0].nope].price jsonpath-ng 1.10.1 (ext) differs on: $..* $.store.book[?@.category == "fiction" && @.price < 10].title $.store.book[?length(@.title) > 15].title $.store.book[?match(@.author, "J.*")].author $.store.book[?search(@.title, "Moby")].title $.store.book[?@.nope == $.store.book[0].nope].price json-p3 and python-jsonpath give identical answers on all queries: True --- jqeq.sh (jq equivalents of six of the queries) --- export PATH="/c/lwdj/json/c4/bin:$PATH" for f in '[.. | .author? // empty]' '[.store.book[] | select(.price < 10) | .title]' '[.. | objects | select(has("isbn")) | .title]' '[.store.book | to_entries[] | select(.key % 2 == 1) | .value.title]' '[.store.book[] | select(.title | length > 15) | .title]' '[path(.. | select(type == "string" and . == "Moby Dick"))]'; do printf '%-74s -> ' "$f"; jq -c "$f" store.json done Output: [.. | .author? // empty] -> ["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"] [.store.book[] | select(.price < 10) | .title] -> ["Sayings of the Century","Moby Dick"] [.. | objects | select(has("isbn")) | .title] -> ["Moby Dick","The Lord of the Rings"] [.store.book | to_entries[] | select(.key % 2 == 1) | .value.title] -> ["Sword of Honour","The Lord of the Rings"] [.store.book[] | select(.title | length > 15) | .title] -> ["Sayings of the Century","The Lord of the Rings"] [path(.. | select(type == "string" and . == "Moby Dick"))] -> [["store","book",2,"title"]] WHY THIS WORKS AS AN ANSWER --------------------------- The expected answers are written down independently of any library (for example 27 values for $..*: store, book, bicycle, four books, eighteen book fields and two bicycle fields), so a score of 21 of 21 for json-p3 and python-jsonpath means they agree with my reading of RFC 9535 and with each other, not just with themselves. The two older libraries are scored on the same questions: jsonpath-plus returns an empty result, without an error, for the unparenthesised filter syntax, the functions, the negative step and the missing-value comparison; jsonpath-ng's ext parser fails on && and the functions and returns 23 instead of 27 for $..*. Object results are reduced to short keys (a title, 'bicycle', 'books[]') so the comparison is not about key order or number formatting. 21 queries on one document is a sample, not the RFC's compliance suite.