Skip to main content
Comparison · 6 min read

JSON Pointer vs JSONPath: Which One to Use

One addresses exactly one value and is a standard. The other queries for many and is not. When each is right, and how the escaping differs.

Both identify something inside a JSON document, and they are easy to confuse because tools often show both. The same value, two notations:

JSONPath   $['users'][0]['e-mail']
Pointer    /users/0/e-mail

The difference is not cosmetic. They were designed for different jobs and only one of them is a standard.

The essential distinction

JSON PointerJSONPath
StandardRFC 6901RFC 9535, published 2024 — most tools predate it
MatchesExactly one value, or nothingZero, one or many
WildcardsNone*, .., slices, unions
FiltersNone?(@.active == true)
Implementations agreeYesNot reliably
Built forAddressingQuerying

A Pointer is an address, like a street address: it names one place, unambiguously, and every reader resolves it identically. A JSONPath is a search, like "every house on this street with a red door" — far more expressive, and the answer depends on the searcher.

Use a Pointer when something must act on the value

Pointers are what the surrounding ecosystem speaks. If you are producing a reference that another system will consume, this is almost always the right choice.

  • JSON Patch (RFC 6902) — every operation names its target with a Pointer.
  • JSON Schema — $ref uses Pointer fragments, and validation errors report the failing location as one.
  • OpenAPI — $ref: '#/components/schemas/User' is a Pointer.
  • Error reporting — "the value at /items/3/price is invalid" is unambiguous and machine-actionable.
[
  { "op": "replace", "path": "/users/0/email", "value": "new@example.com" },
  { "op": "remove",  "path": "/users/2" }
]

You could not write that with JSONPath. An operation that might match three values is not an operation — it is ambiguous, and a patch format cannot be ambiguous.

Use JSONPath when you are looking for something

Everything a Pointer deliberately cannot do:

$.users[*].email                  every email
$..id                             every "id" at any depth
$.users[?(@.active == true)]      every active user
$.users[-1:]                      the last user
$.users[0,2]                      a union of two

This is exploration and extraction — pulling a field out of every record, finding where a value lives, filtering a collection. The JSONPath evaluator runs these against your document and shows every match, with both notations for each result.

The escaping differs, and it matters

Pointer uses / as its separator and ~ as its escape character, so both need escaping when they appear in a key. Two rules, applied in this order when reading: ~1 means /, and ~0 means ~.

key "a/b", then key "c~d"

Pointer    /a~1b/c~0d
JSONPath   $['a/b']['c~d']

The order matters when unescaping. Replace ~1 first and then ~0, and a literal ~1 in a key would be corrupted — so the rule is always ~1 → / before ~0 → ~. Most bugs in hand-written Pointer code are exactly this.

JSONPath avoids the question by using bracket notation with quoted keys, which is why results here are normalised to brackets rather than echoing the dot notation you typed.

Two more things worth knowing

A Pointer to the whole document is the empty string, not /. That trips people up: "" means the root, while "/" means the key whose name is the empty string — which is legal JSON and a genuinely different thing.

Array indices in a Pointer are plain numbers, and - means "the position after the last element", used by JSON Patch to append. There is no slicing and no negative indexing; /users/-1 is a key literally named -1, not the last item.

Choosing

  1. Will the result be consumed by another system — a patch, a schema, an error report? Pointer. It is standardised and everything understands it.
  2. Do you need more than one match, a filter or a wildcard? JSONPath. A Pointer cannot express it.
  3. Are you exploring a document to find something? JSONPath, then take the Pointer of the match you wanted for whatever you build next.

That third step is the common workflow and it is why both are shown side by side: query loosely to find the thing, then address it precisely. Click any node in the tree view to copy its path directly.