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.
| Pointer | Resolves to |
|---|---|
"" (empty) | the whole document |
/users | the 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
$refvalues such as"#/$defs/address". The part after#is a JSON Pointer in URI fragment form, which is why/inside keys turns into~1there 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.
| Syntax | Meaning | Example |
|---|---|---|
$ | 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
| Task | JSON Pointer | JSONPath | jq |
|---|---|---|---|
| First user's name | /users/0/name | $.users[0].name | .users[0].name |
| All user names | not possible | $.users[*].name | .users[].name |
| Users older than 30 | not possible | $.users[?@.age > 30].name | .users[] | select(.age > 30) | .name |
| Every id anywhere | not possible | $..id | [.. | .id? | select(. != null)] |
| Last user | not 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/0is 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 nestedid, not just the top-level one). - Keys with dots.
$.config.app.namelooks forconfig, thenapp, thenname. A single key literally calledapp.nameneeds 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 message | JSON Pointer |
| extract values matching a condition from an API response | JSONPath |
| store a query in config that several languages will evaluate | JSONPath (RFC 9535 subset) |
| reshape, aggregate, or script on the command line | jq |
| filter output from the AWS or Azure CLI | JMESPath |
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.