Skip to content
Advanced

JSONPath vs JSON Pointer: Querying JSON Without Writing Loops

·8 min read
JSONPath vs JSON Pointer: Querying JSON Without Writing Loops - Advanced guide from JSON Prettifier

Quick answer: JSON Pointer (RFC 6901) is an address for one location in a document, written like a file path: /users/0/name. JSONPath (RFC 9535) is a query language that can match many locations with wildcards, recursion, and filters: $.users[?@.age > 30].name. Use Pointer when you need to point at a single field (JSON Patch, JSON Schema $ref); use JSONPath when you need to search.

You have a 4,000-line API response and need the name of every user with an expired subscription. You could write a nested loop. Or you could write one expression that describes what you want and let a library walk the tree. That is what JSONPath and JSON Pointer are for, and they are not interchangeable: one is a precise address, the other is a search.

Every example below uses this document:

{
  "users": [
    { "id": 1, "name": "Priya", "age": 34, "roles": ["admin", "dev"] },
    { "id": 2, "name": "Marco", "age": 27, "roles": ["dev"] },
    { "id": 3, "name": "Aiko",  "age": 41, "roles": [] }
  ],
  "meta": { "a/b": "slash key", "m~n": "tilde key" }
}

Run a sample expression in the JSONPath Query Tester, or explore the same nested data in the JSON Viewer.

JSON Pointer (RFC 6901): One Path, One Value

A JSON Pointer is a string of tokens separated by /. Each token is either an object key or an array index. There are no wildcards, no conditions, no functions. It resolves to exactly one value or fails.

PointerResolves to
"" (empty)the whole document
/usersthe array of three users
/users/0/name"Priya"
/users/2/roles[]
/users/-the slot after the last element (JSON Patch add only)
/meta/a~1b"slash key"
/meta/m~0n"tilde key"

The two escape sequences

Because / separates tokens, a key that contains a slash needs escaping. RFC 6901 defines exactly two sequences: ~1 means / and ~0 means ~. The decoding order matters. Replace ~1 first, then ~0. If you do it the other way round, the token ~01 decodes to / instead of the literal string ~1.

Array indices are plain decimal digits with no leading zeros, so /users/00 is invalid. Negative indices are not allowed either; only - has a special meaning, and only when adding to the end of an array.

A pointer resolver in 12 lines

Most languages have a package (json-pointer or jsonpointer on npm, jsonpointer on PyPI), but the algorithm is short enough to write yourself:

function resolvePointer(doc, pointer) {
  if (pointer === "") return doc;
  if (pointer[0] !== "/") throw new Error("Invalid JSON Pointer: " + pointer);
  return pointer
    .slice(1)
    .split("/")
    .map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"))
    .reduce((cur, token) => {
      if (cur === undefined || cur === null) return undefined;
      return Array.isArray(cur) ? cur[Number(token)] : cur[token];
    }, doc);
}

resolvePointer(data, "/users/0/name"); // "Priya"
resolvePointer(data, "/meta/a~1b");    // "slash key"
resolvePointer(data, "/users/9/name"); // undefined

Where you already use JSON Pointer

  • JSON Patch operations: { "op": "replace", "path": "/users/1/age", "value": 28 }. The JSON Patch vs Merge Patch guide shows the full operation set.
  • JSON Schema and OpenAPI $ref values such as "#/$defs/address". The part after # is a JSON Pointer in URI fragment form, which is why / inside keys turns into ~1 there too. See JSON Schema explained and, for how the two dialects differ, JSON Schema vs OpenAPI schema.
  • Validation error reports: most schema validators report the failing location as a pointer, e.g. instancePath: "/users/2/age".

JSONPath (RFC 9535): Query Many Values at Once

JSONPath began as a 2007 blog post by Stefan Goessner that borrowed the feel of XPath. For seventeen years every library implemented its own flavor. In February 2024 the IETF published RFC 9535, which pins down the grammar. The basics are the same in every implementation; filters are where older libraries diverge.

SyntaxMeaningExample
$root node$
.name or ['name']child by key$.meta['a/b']
[0]array index (negative counts from end)$.users[-1]
[0:2]slice, end exclusive$.users[0:2].name
[*] or .*wildcard, every child$.users[*].name
..recursive descent, any depth$..id
[0,2]union of selectors$.users[0,2]
[?expr]filter; @ is the current node$.users[?@.age > 30]

Filters in practice

Against the sample document:

$.users[?@.age > 30].name          -> ["Priya", "Aiko"]
$.users[?@.roles[0] == "admin"].id -> [1]
$.users[?length(@.roles) == 0].name -> ["Aiko"]
$.users[?@.name =~ "^A"]            -> non-standard; RFC 9535 uses match()
$.users[?match(@.name, "A.*")].name -> ["Aiko"]

RFC 9535 ships a small function set: length(), count(), match(), search(), and value(). It also allows the older parenthesised form [?(@.age > 30)] because the parentheses are just grouping. If your library predates the RFC it may support =~ for regex or arbitrary script expressions; those are extensions, not the standard, and will not port.

JavaScript: jsonpath-plus

import { JSONPath } from "jsonpath-plus";

const names = JSONPath({ path: "$.users[?(@.age > 30)].name", json: data });
// ["Priya", "Aiko"]

// Get the pointers to the matches instead of the values:
JSONPath({ path: "$..id", json: data, resultType: "pointer" });
// ["/users/0/id", "/users/1/id", "/users/2/id"]

That resultType: "pointer" option is the bridge between the two standards: run a JSONPath search to find things, then use the returned JSON Pointers to patch them.

Python: jsonpath-ng

from jsonpath_ng.ext import parse

expr = parse("$.users[?(@.age > 30)].name")
print([m.value for m in expr.find(data)])
# ['Priya', 'Aiko']

# Each match also knows where it came from:
print([str(m.full_path) for m in expr.find(data)])
# ['users.[0].name', 'users.[2].name']

Note the .ext import. The base jsonpath_ng parser does not support filter expressions; the extended one does.

Side by Side: The Same Queries in Pointer, JSONPath, and jq

TaskJSON PointerJSONPathjq
First user's name/users/0/name$.users[0].name.users[0].name
All user namesnot possible$.users[*].name.users[].name
Users older than 30not possible$.users[?@.age > 30].name.users[] | select(.age > 30) | .name
Every id anywherenot possible$..id[.. | .id? | select(. != null)]
Last usernot possible$.users[-1].users[-1]
Key containing a slash/meta/a~1b$.meta['a/b'].meta["a/b"]
Whole document(empty string)$.

The "not possible" cells are the whole story. JSON Pointer cannot select more than one node by design, which is a feature when you want an unambiguous address and a limitation when you want to search.

jq: The Third Option

On a terminal, jq usually beats both. It is a complete language: you can filter, map, group, and reshape in one pipeline, and it reads from stdin so it chains with curl:

curl -s https://api.example.com/users \
  | jq '[.users[] | select(.age > 30) | {id, name}]'

The trade-off is portability. A jq program lives in a shell; a JSONPath string can be stored in a YAML config, passed as an API parameter, or evaluated by a Java and a JavaScript service identically. If you want the full command reference, see the jq cheat sheet.

Dialects You Will Meet in the Wild

kubectl -o jsonpath

Kubernetes has its own JSONPath template dialect. Expressions are wrapped in curly braces, the leading $ is optional, and it adds range/end for iteration:

kubectl get pods -o jsonpath='{.items[*].metadata.name}'

kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.phase}{"\n"}{end}'

Filters work ({.items[?(@.status.phase=="Running")].metadata.name}) but the regex and function extensions do not. Treat it as JSONPath-shaped, not JSONPath-compliant.

AWS CLI --query is JMESPath, not JSONPath

This one catches people constantly. aws ... --query uses JMESPath, a different language with no leading $, no recursive descent, and its own filter syntax:

aws ec2 describe-instances \
  --query 'Reservations[].Instances[?State.Name==`running`].InstanceId' \
  --output text

Backticks mark literals in JMESPath, and functions like sort_by and join are built in. Azure CLI uses JMESPath as well. If a JSONPath expression "does nothing" in a cloud CLI, this is why.

API test frameworks

If you have written assertions in REST Assured, Karate, or similar tools, you have probably seen JSONPath-style paths. Each embeds a specific library (Jayway JsonPath is common on the JVM) with its own pre-RFC quirks, so test the filter syntax before relying on it.

Common Mistakes

  • Starting a pointer without a slash. users/0 is not a valid JSON Pointer; it must be /users/0. The empty string, not /, means the root. / actually points to a key named "".
  • Expecting JSONPath to return a single value. It always returns a list of matches, even for $.users[0].name. Take [0] from the result or check for an empty list.
  • Using .. on large documents without thinking. Recursive descent visits every node. On a 50 MB response it is slow and may match keys you did not intend (every nested id, not just the top-level one).
  • Keys with dots. $.config.app.name looks for config, then app, then name. A single key literally called app.name needs bracket notation: $.config['app.name']. The same problem appears when you flatten nested JSON into dotted keys.
  • Querying invalid JSON. Every library parses the document first, and a stray trailing comma fails before your path runs. Paste the response into JSON Prettifier to validate it and, with Sort Keys, to see the key names you are about to query in alphabetical order.

Which One Should You Use?

You need to...Use
reference one field in a patch, schema, or error messageJSON Pointer
extract values matching a condition from an API responseJSONPath
store a query in config that several languages will evaluateJSONPath (RFC 9535 subset)
reshape, aggregate, or script on the command linejq
filter output from the AWS or Azure CLIJMESPath

A practical pattern that uses both standards: search with JSONPath, get the result as pointers, and feed those pointers into a JSON Patch. You get the expressiveness of a query and the precision of an address.

Before changing evaluators, run these four JSONPath migration checks to compare filter meaning, selected locations, result wrapping, and duplicate behavior rather than relying on syntax acceptance alone.

Frequently Asked Questions

What is the difference between JSONPath and JSON Pointer?
JSON Pointer (RFC 6901) identifies exactly one location in a document, such as /users/0/name, and cannot express wildcards or conditions. JSONPath (RFC 9535) is a query language that can return many values using wildcards, recursive descent, slices, and filter expressions like $.users[?@.age > 30].
How do I escape a slash in a JSON Pointer?
Write ~1 for a forward slash and ~0 for a tilde. A key named a/b is addressed as /a~1b, and a key named m~n as /m~0n. Decode ~1 first and then ~0, otherwise ~01 is decoded incorrectly.
Is JSONPath a standard?
Yes, since February 2024. RFC 9535 standardized the syntax that Stefan Goessner proposed in 2007. Older libraries predate the RFC and differ in details such as filter syntax, so check your library's documentation for anything beyond basic paths.
Does kubectl use JSONPath?
kubectl supports -o jsonpath, but it is a Kubernetes-specific dialect: expressions are wrapped in curly braces, the leading $ is optional, and it adds range/end for iteration. The AWS CLI's --query option is different again; it uses JMESPath, not JSONPath.
Should I use jq instead of JSONPath?
On the command line, usually yes. jq is a full programming language that can filter, reshape, and aggregate JSON. JSONPath is better when you need a query string that lives inside another system, such as a config file, a test assertion, or an API parameter.

See the Structure Before You Query It

Paste a response, format it, and sort the keys so the path you need is obvious. Nothing leaves your browser.

Format My JSON