JSON
Reading JSON paths in a diff: $.settings.theme, $.members[2] and friends
A JSON path in a diff tells you exactly where a change happened. $ is the root of the document, .name steps into an object key, [2] steps into an array at a zero-based index, and ["odd key"] is used when a key is not a simple identifier. So $.members[2] means "the third item of the members array at the top level".
This guide explains the notation as it appears in JSON Mirror and most other JSON diff tools, shows how it maps to JSON Pointer, jq, JavaScript and Python, and how to jump from a path in a report straight to the value in your own data.
What a JSON diff path looks like
Take this pair of documents:
// Original
{"project":"JSON Mirror","version":"1.0","settings":{"theme":"light","notifications":true},"members":["Alex","Sam"],"status":"draft"}
// Updated
{"project":"JSON Mirror","version":"1.1","settings":{"theme":"dark","notifications":true},"members":["Alex","Sam","Jordan"],"published":true}
Comparing them produces five changes, each identified by a path:
- changed
$.version:"1.0"became"1.1" - changed
$.settings.theme:"light"became"dark" - added
$.members[2]:"Jordan" - removed
$.status - added
$.published:true
Read each path left to right as a set of directions: start at the root, then follow each step. $.settings.theme means "root, then key settings, then key theme".
The $ root symbol
The $ comes from JSONPath, a query notation that has been used informally for years and was standardized as RFC 9535 in February 2024. In JSONPath, $ is the root node: the entire parsed document.
If the whole document is a single primitive, such as 42 or "hello", there is nothing to step into. A change from 42 to 43 is reported at the path $ alone. The same happens when the top-level type changes, for example from an object to an array: it is one change at $.
Dot notation vs. bracket notation
Paths use three kinds of step.
Dot notation for identifier-like keys
$.settings.theme is the readable form for ordinary keys. JSON Mirror uses a dot only when the key matches the regular expression /^[A-Za-z_$][\w$]*$/: a letter, underscore or dollar sign, followed by letters, digits, underscores or dollar signs. That covers $.user_id, $.createdAt and $.$ref.
Bracket index notation for arrays
Array elements are addressed by position in square brackets, counting from zero. $.members[0] is "Alex", $.members[1] is "Sam", and $.members[2] is "Jordan". Steps chain naturally: $.orders[0].items[3].sku.
One caution: an index is a position, not an identity. JSON Mirror compares arrays by index, so inserting an item near the start shifts every later element and can produce several "changed" entries plus one "added" at the end. If your arrays hold records with IDs, read Comparing JSON arrays: by position or by identity? before trusting index paths.
Quoted bracket notation for unusual keys
Any key that fails the identifier test is written as a quoted string inside brackets. That includes keys with:
- spaces:
$["first name"] - dashes:
$["first-name"] - dots:
$["a.b"](this is one key nameda.b, not keybinside keya) - a leading digit:
$["2fa"] - an empty name: the key
""becomes$[""]
The quoted part uses JSON string escaping, so a key containing a double quote appears as $["say \"hi\""]. The distinction between $["a.b"] and $.a.b is the main reason brackets exist: without them, the two paths would look identical.
A note on strictness: RFC 9535's dot shorthand allows letters, digits, underscores and non-ASCII characters, but not $. If you paste a path like $.$ref into a strict JSONPath implementation, write it as $["$ref"] instead. The bracket form is always valid.
JSONPath vs. JSON Pointer
JSON Pointer (RFC 6901) is the other common way to address a value. It is what JSON Schema $ref fragments and JSON Patch operations use. The differences:
- JSONPath is a query language; it can also express wildcards and filters. A diff only uses its simplest form, a single concrete location.
- JSON Pointer identifies exactly one value. The root is the empty string
"", and each step is a/followed by a key or index:/members/2. - JSON Pointer has no quoting. Instead,
~in a key is written~0and/is written~1. A key nameda/bis/a~1b. - Array indexes are zero-based in both.
Because JSON Pointer does not use dots as separators, keys like a.b or first-name need no special treatment: /a.b, /first-name.
Translation table: JSONPath, JSON Pointer, jq, JavaScript, Python
The same locations in each notation. data is the parsed document.
| Diff path (JSONPath) | JSON Pointer | jq | JavaScript | Python |
|---|---|---|---|---|
$ | "" (empty) | . | data | data |
$.settings.theme | /settings/theme | .settings.theme | data.settings.theme | data["settings"]["theme"] |
$.members[2] | /members/2 | .members[2] | data.members[2] | data["members"][2] |
$["first-name"] | /first-name | .["first-name"] | data["first-name"] | data["first-name"] |
$["a.b"] | /a.b | .["a.b"] | data["a.b"] | data["a.b"] |
$["a/b"] | /a~1b | .["a/b"] | data["a/b"] | data["a/b"] |
$[""] | / | .[""] | data[""] | data[""] |
$.$ref | /$ref | .["$ref"] | data.$ref | data["$ref"] |
Two rows deserve a second look. The JSON Pointer / is not the root; it points to the key named "", because the root pointer is the empty string. And in jq, $ starts a variable name, so a key like $ref needs the bracket form.
How to jump from a diff path to the value
Once a diff has told you where something changed, you often want to look at the surrounding data or check the value in another file. Translate the path one step at a time.
With jq
jq paths look almost identical to JSONPath; drop the leading $:
jq '.members[2]' updated.json
"Jordan"
jq '.settings' original.json
{
"theme": "light",
"notifications": true
}
jq '.["first-name"]' user.json
The single quotes above are for POSIX shells such as bash and zsh; other shells may need different quoting.
With JavaScript
const data = JSON.parse(text);
data.settings.theme; // "dark"
data.members[2]; // "Jordan"
data["first-name"]; // bracket form for unusual keys
Since JSON Mirror's dot rule follows JavaScript identifier syntax for ASCII keys, any path it prints can be pasted after data by replacing the $.
With Python
import json
with open("updated.json") as f:
data = json.load(f)
data["settings"]["theme"] # 'dark'
data["members"][2] # 'Jordan'
Python has no dot access for dictionaries, so every key step becomes ["key"] and every index step stays [n].
Reading paths on added and removed entries
The path of an "added" entry exists only in the updated document; the path of a "removed" entry exists only in the original. Running jq '.status' updated.json on the example above prints null, because jq returns null for a missing key. That is easy to misread: a missing key and an explicit null are different things, and JSON Mirror reports them differently. null vs. missing: the JSON difference that breaks APIs covers why that matters. In jq, has("status") tells the two apart.
When a whole object or array is added or removed, the diff shows one entry at the subtree's path containing the full value, rather than one entry per nested key.
Try it
Paste two versions of a document into JSON Mirror and every change comes back with its path, a one-line explanation and the before and after values. Everything runs in your browser tab, and nothing is uploaded. Use the table above to turn any path into a jq filter or a line of code, and remember the two rules that trip people up most: indexes start at zero, and quoted brackets mean "this is one key, read it literally".
Frequently asked questions
What does the $ mean in a JSON path?
The $ stands for the root of the document, the whole parsed JSON value. Every path in JSONPath notation starts there, so $.version means the version key of the top-level object, and a bare $ means the document itself.
Are array indexes in JSON paths zero-based?
Yes. In JSONPath, JSON Pointer, jq, JavaScript and Python, the first array element is index 0. So $.members[2] is the third element of the members array.
What is the difference between JSONPath and JSON Pointer?
JSONPath (RFC 9535) is a query language whose paths start with $ and use dots and brackets, such as $.members[2]. JSON Pointer (RFC 6901) is a simpler string syntax that identifies exactly one value with slash-separated segments, such as /members/2, escaping ~ as ~0 and / as ~1.
Why does a diff path use brackets instead of a dot for some keys?
Dot notation only works for keys that look like identifiers. Keys with spaces, dashes, dots, a leading digit, or an empty name are written in quoted brackets, such as $["first-name"] or $["a.b"], so the path cannot be misread.
How do I get the value at a JSON path from the command line?
Translate the path into a jq filter and run it against the file, for example jq '.members[2]' updated.json. Keys that need brackets in the path also need them in jq, as in jq '.["first-name"]' data.json.