json1-8 Exercise 1: Resolving the RFC 6901 Pointers in Four Implementations ============================================================================ 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 ex1.mjs, then python ex1.py (it reads the Node results and runs jq itself). --- rfc6901.json (the RFC 6901 section 5 document; backslash and quote keys were written by a script, not typed) --- {"foo": ["bar", "baz"], "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8} --- ptrs.json (18 pointers: the RFC's twelve and six edge cases) --- ["", "/foo", "/foo/0", "/foo/1", "/", "/a~1b", "/c%d", "/e^f", "/g|h", "/i\\j", "/k\"l", "/ ", "/m~0n", "/foo/-", "/foo/01", "/foo/2", "/nope", "/foo/bar"] --- ptr.jq (my pointer function for jq) --- # JSON Pointer (RFC 6901) as a jq path: split on "/", unescape ~1 then ~0. # Careful: "" is the whole document, but "/" is the key "" (an empty token). def ptr_path: if . == "" then [] else .[1:] as $rest | (if $rest == "" then [""] else ($rest | split("/")) end) | map(gsub("~1"; "/") | gsub("~0"; "~")) end; # Digit-only tokens are array indexes only when the value there is an array (no leading zeros); otherwise they are keys. def ptr_get($p): reduce ($p | ptr_path)[] as $t (.; if type == "array" then (if ($t | test("^(0|[1-9][0-9]*)$")) then .[($t | tonumber)] else error("not an array index: \($t)") end) else .[$t] end); --- ex1.mjs --- import fs from 'node:fs'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const jp3 = await import('json-p3'); const fjp = require('fast-json-patch'); const doc = JSON.parse(fs.readFileSync('rfc6901.json', 'utf8')); const ptrs = JSON.parse(fs.readFileSync('ptrs.json', 'utf8')); const out = {}; for (const p of ptrs) { const r = {}; try { r['json-p3'] = JSON.stringify(jp3.resolve(p, doc)); } catch (e) { r['json-p3'] = 'NOVALUE'; } try { const v = fjp.getValueByPointer(doc, p); r['fast-json-patch'] = v === undefined ? 'NOVALUE' : JSON.stringify(v); } catch (e) { r['fast-json-patch'] = 'NOVALUE'; } out[p] = r; } fs.writeFileSync('ptr_node.json', JSON.stringify(out)); console.log('Node results written for ' + ptrs.length + ' pointers'); --- ex1.py --- import json, subprocess, jsonpointer doc = json.load(open('rfc6901.json', encoding='utf-8')) ptrs = json.load(open('ptrs.json', encoding='utf-8')) node = json.load(open('ptr_node.json', encoding='utf-8')) JQ = r'C:\lwdj\json\c4\bin\jq.exe' dumps = lambda v: json.dumps(v, ensure_ascii=False, separators=(',', ':')) # what RFC 6901 section 5 says for the first twelve; the last five have no value whole = dumps(doc) exp = {'': whole, '/foo': '["bar","baz"]', '/foo/0': '"bar"', '/foo/1': '"baz"', '/': '0', '/a~1b': '1', '/c%d': '2', '/e^f': '3', '/g|h': '4', ('/i' + chr(92) + 'j'): '5', '/k"l': '6', '/ ': '7', '/m~0n': '8', '/foo/-': 'NOVALUE', '/foo/01': 'NOVALUE', '/foo/2': 'NOVALUE', '/nope': 'NOVALUE', '/foo/bar': 'NOVALUE'} def py(p): try: v = jsonpointer.resolve_pointer(doc, p) return 'NOVALUE' if isinstance(v, jsonpointer.EndOfList) else dumps(v) except Exception: return 'NOVALUE' def jq(p): prog = 'include "ptr"; ptr_get(' + json.dumps(p) + ')' r = subprocess.run([JQ, '-c', '-L', '.', prog, 'rfc6901.json'], capture_output=True, text=True, encoding='utf-8') if r.returncode != 0: return 'NOVALUE' o = r.stdout.strip(); return 'NOVALUE' if o == 'null' else o libs = ['json-p3', 'fast-json-patch', 'jsonpointer (Python)', 'jq function'] res = {p: {'json-p3': node[p]['json-p3'], 'fast-json-patch': node[p]['fast-json-patch'], 'jsonpointer (Python)': py(p), 'jq function': jq(p)} for p in ptrs} print('pointer'.ljust(12) + 'RFC says'.ljust(14) + ''.join(l[:15].ljust(17) for l in libs)) wrong = {l: [] for l in libs} for p in ptrs: e = exp[p]; cells = [] for l in libs: r = res[p][l]; ok = r == e; cells.append(('ok' if ok else 'DIFFERS: ' + r[:12]).ljust(17)) if not ok: wrong[l].append(p) print(json.dumps(p).ljust(12) + (e if len(e) < 12 else 'whole doc' if e == whole else e[:11]).ljust(14) + ''.join(cells)) print() for l in libs: print(l.ljust(24), 'matches the RFC on %d of %d pointers' % (len(ptrs) - len(wrong[l]), len(ptrs)), '' if not wrong[l] else '| differs on: ' + ', '.join(json.dumps(w) for w in wrong[l])) Output of node ex1.mjs: Node results written for 18 pointers Output of python ex1.py: pointer RFC says json-p3 fast-json-patch jsonpointer (Py jq function "" whole doc ok ok ok ok "/foo" ["bar","baz ok ok ok ok "/foo/0" "bar" ok ok ok ok "/foo/1" "baz" ok ok ok ok "/" 0 ok ok ok ok "/a~1b" 1 ok ok ok ok "/c%d" 2 ok ok ok ok "/e^f" 3 ok ok ok ok "/g|h" 4 ok ok ok ok "/i\\j" 5 ok ok ok ok "/k\"l" 6 ok ok ok ok "/ " 7 ok ok ok ok "/m~0n" 8 ok ok ok ok "/foo/-" NOVALUE ok ok ok ok "/foo/01" NOVALUE ok DIFFERS: "baz" ok ok "/foo/2" NOVALUE ok ok ok ok "/nope" NOVALUE ok ok ok ok "/foo/bar" NOVALUE ok ok ok ok json-p3 matches the RFC on 18 of 18 pointers fast-json-patch matches the RFC on 17 of 18 pointers | differs on: "/foo/01" jsonpointer (Python) matches the RFC on 18 of 18 pointers jq function matches the RFC on 18 of 18 pointers WHY THIS WORKS AS AN ANSWER --------------------------- The expected value for each of the first twelve pointers is copied from RFC 6901 section 5 (I checked the document and the table against the RFC text); the six edge cases are expected to produce no value. Each library's way of saying no value (an exception, undefined, a marker object) is normalised to NOVALUE so the columns are comparable. json-p3, Python jsonpointer and my jq function match on all 18; fast-json-patch returns "baz" for /foo/01, which the RFC forbids (no leading zeros). My jq function originally treated "/" as the whole document (in jq, "" | split("/") is an empty list); the table caught it, and the version shown handles the empty-token case explicitly. Tested with these libraries only, on one small document.