BytePane

JSON Path Finder: Query & Extract JSON Data with JSONPath

JSON16 min read

The 3 AM Problem: 2,000 Lines of JSON, One Value You Need

A Kubernetes cluster alert fires at 3 AM. The kubectl describe pod command dumps 2,000 lines of JSON. The value you need — the container restart count — is buried inside status.containerStatuses[0].restartCount. You could grep through the output, write a five-line Python script, or pipe it through jq. Or you could write one JSONPath expression:

$.status.containerStatuses[0].restartCount
# Returns: 42

That is JSONPath — a query language for JSON, analogous to XPath for XML. Stefan Gössner introduced it in a 2007 blog post titled “JSONPath — XPath for JSON.” The post spawned approximately 50 library implementations across programming languages before the IETF finally published a formal specification: RFC 9535, in February 2024 — exactly 17 years later.

This guide covers everything you need: the complete RFC 9535 syntax with worked examples, filter expressions for querying by value rather than structure, production-ready code in JavaScript, Python, and Go, a library comparison table, and how JSONPath compares to JMESPath, jq, and JSONata. To test expressions live against any JSON document, use BytePane's JSON path tester.

Key Takeaways

  • JSONPath standardized as RFC 9535 in February 2024, 17 years after Gössner's 2007 blog post that spawned ~50 library implementations. RFC 9535 resolves long-standing divergence between implementations on edge cases.
  • $ is the root. .name for child access. [*] for wildcard. ..name for recursive descent at any depth. [?(@.price < 10)] for filter by value.
  • jsonpath-plus (npm) is the most widely used JS library; jsonpath-ng tracks RFC compliance in Python; gjson is 5–10× faster in Go with its own path syntax.
  • RFC 9535 adds five built-in functions: length(), count(), match(), search(), and value(). Regex patterns use I-Regexp (RFC 9485), not full PCRE.
  • RFC 9535 comparisons are type-safe: comparing a number to a string returns false, never coerces. A JSONPath query always returns a nodelist — never null, never a scalar directly.

RFC 9535: The Standard That Took 17 Years

Before RFC 9535, JSONPath existed as a de facto standard with no normative specification. Different libraries diverged on edge cases: how to handle negative array indexes, whether filter expressions were required, what happened when a path matched nothing. In 2019, Christoph Burgmer created the JSON Path Comparison project, which documented the divergences across implementations and made the case for a formal specification.

The IETF JSONPath working group (informally named “jsonpath”) spent several years resolving these conflicts. Per the IETF's own blog post on the RFC, the process required the working group to define a portable subset of regular expressions — I-Regexp (RFC 9485) — because full PCRE is not consistently implementable across all target languages. Key decisions baked into RFC 9535:

  • Always returns a nodelist: A JSONPath query always returns a list. No match = empty list. Never throws, never returns null.
  • Strict type comparisons in filters: Comparing a string to a number in a filter expression always returns false — no coercion, no JavaScript-style loose equality.
  • Five standardized functions: length(), count(), match(), search(), and value() are defined with precise semantics.
  • Compliance test suite: The RFC ships with a CTS (Compliance Test Suite) that implementations can run to verify conformance. As of January 2026, the Blazing.Json.JSONPath .NET library achieved 100% CTS certification.

The working group is now dormant — RFC 9535 is the terminal deliverable. Topics deferred during the working group process (such as additional function extensions) are tracked in the spec repository for potential future RFCs.

Complete JSONPath Syntax Reference (RFC 9535)

Every JSONPath expression starts with $, the root node. From there, you chain segments that select child or descendant nodes. Here is the complete RFC 9535 selector reference:

SelectorSyntaxDescriptionExample
Root$The entire root JSON value$
Child (dot).nameSelect child by name$.store.name
Child (bracket)['name']Child by name — handles keys with spaces or special chars$['my-key']
Array index[n]0-based index; negative counts from end$.items[0] $.items[-1]
Wildcard.* or [*]All direct children of current node$.store.* $.items[*]
Slice[start:end:step]Python-style array slice (start inclusive, end exclusive)$.items[0:5] $.items[::2]
Union[a,b,c]Multiple name or index selectors$['name','price'] $[0,2,4]
Recursive descent..nameAll matching nodes at any depth in the tree$..price $..author
Filter[?expr]Filter nodes — @ refers to the current node$[?(@.price < 10)]

Worked Examples: Bookstore API Response

JSONPath expressions — original example from Gössner's 2007 blog post
// JSON document
const store = {
  "store": {
    "book": [
      { "category": "reference", "author": "Nigel Rees",       "title": "Sayings",               "price": 8.95  },
      { "category": "fiction",   "author": "Evelyn Waugh",     "title": "Sword of Honour",        "price": 12.99 },
      { "category": "fiction",   "author": "Herman Melville",  "title": "Moby Dick",              "price": 8.99,  "isbn": "0-553-21311-3" },
      { "category": "fiction",   "author": "J. R. R. Tolkien", "title": "The Lord of the Rings",  "price": 22.99, "isbn": "0-395-19395-8" }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  }
}

// $.store.book[*].author      → All book authors (array)
// ["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"]

// $..author                   → All authors via recursive descent (same result here)

// $.store.book[2]             → Third book (0-indexed)
// {"category":"fiction","author":"Herman Melville",...}

// $.store.book[-1]            → Last book
// {"author":"J. R. R. Tolkien",...}

// $.store.book[0,1]           → First and second book (union)

// $.store.book[?(@.isbn)]     → Only books with an isbn field
// [Moby Dick, Lord of the Rings]

// $.store.book[?(@.price<10)] → Books under $10
// [Sayings ($8.95), Moby Dick ($8.99)]

// $..price                    → All prices anywhere in doc (books + bicycle)
// [8.95, 12.99, 8.99, 22.99, 19.95]

// $.store.book[*]['title','price']  → Title and price for every book (union in bracket)

Filter Expressions: Query by Value, Not Just Structure

Filter expressions — [?expr] — are the most powerful JSONPath feature. The @ symbol refers to the current node being evaluated against each element. RFC 9535 defines comparison operators, logical operators, and five built-in functions usable in filter expressions:

RFC 9535 filter expression operators and built-in functions
// Comparison operators (type-safe — mixing types returns false, not coerced)
$[?(@.price == 8.95)]        // Equal
$[?(@.price != 8.95)]        // Not equal
$[?(@.price < 10)]           // Less than
$[?(@.price >= 10)]          // Greater than or equal
$[?(@.status == "active")]   // String comparison

// Existence test (no operator = boolean — does the key exist?)
$[?(@.isbn)]                 // Items that HAVE an isbn field
$[?([email protected])]                // Items that do NOT have isbn

// Logical operators
$[?(@.price < 10 && @.category == "fiction")]    // AND
$[?(@.price < 10 || @.category == "reference")]  // OR

// RFC 9535 built-in functions
$[?(length(@.title) > 10)]          // length(): string length or array count
$[?(count(@.tags) > 0)]             // count(): number of nodes in nodelist
$[?(match(@.title, "Moby.*"))]      // match(): full-string I-Regexp match
$[?(search(@.title, "Lord"))]       // search(): substring I-Regexp search
// value(): extracts a value from a nodelist for comparison (e.g. nested path)

// Nested path in filter
$[?(@.address.city == "London")]    // Compare nested field
$[?(@.scores[0] >= 90)]             // Compare first array element

RFC 9535 type safety: Pre-RFC implementations often coerced types in comparisons — $[?(@.count == "5")] might match count:5 (number) in some libraries. RFC 9535 forbids this. A number-to-string comparison always returns false. This is a breaking change from many pre-2024 implementations — test your existing JSONPath expressions against new RFC-compliant libraries before migrating.

I-Regexp in match() and search()

The match() and search() functions use I-Regexp (RFC 9485), a portable subset of regular expressions. I-Regexp supports basic character classes, quantifiers (*, +, ?, {n,m}), anchoring (^, $), and Unicode categories — but not lookaheads, backreferences, or PCRE extensions. Key difference between the two functions:

  • match(@.field, "pattern") — the pattern must match the entire string (implicit anchoring at both ends).
  • search(@.field, "pattern") — the pattern matches anywhere within the string (like regex find).

JSONPath in Production Code

JavaScript: jsonpath-plus

jsonpath-plus — browser and Node.js
import { JSONPath } from 'jsonpath-plus'

const data = {
  users: [
    { id: 1, name: 'Alice', role: 'admin', score: 92, active: true  },
    { id: 2, name: 'Bob',   role: 'user',  score: 74, active: false },
    { id: 3, name: 'Charlie', role: 'admin', score: 88, active: true },
  ]
}

// Extract all names
const names = JSONPath({ path: '$.users[*].name', json: data })
// ['Alice', 'Bob', 'Charlie']

// Filter: active admins only
const activeAdmins = JSONPath({
  path: '$.users[?(@.role=="admin" && @.active==true)]',
  json: data,
})
// [{id:1, name:'Alice',...}, {id:3, name:'Charlie',...}]

// Single value (wrap: false returns first result instead of array)
const topScorer = JSONPath({
  path: '$.users[?(@.score > 90)].name',
  json: data,
  wrap: false,
})
// 'Alice'

// resultType: 'path' — returns the path of each match, not the value
const paths = JSONPath({ path: '$..name', json: data, resultType: 'path' })
// ["$['users'][0]['name']", "$['users'][1]['name']", "$['users'][2]['name']"]

// resultType: 'all' — returns {path, value, parent, parentProperty} for each match
const matches = JSONPath({ path: '$.users[*]', json: data, resultType: 'all' })
matches.forEach(m => console.log(m.path, m.value.name))

Python: jsonpath-ng

jsonpath-ng — RFC 9535 compliance in Python
from jsonpath_ng import parse
# For filter expressions: import the ext subpackage
from jsonpath_ng.ext import filter  # noqa: F401 — enables [?(...)] support

data = {
    "orders": [
        {"id": "A1", "status": "shipped",   "total": 59.99,  "items": 3},
        {"id": "A2", "status": "pending",   "total": 129.50, "items": 7},
        {"id": "A3", "status": "delivered", "total": 29.99,  "items": 1},
        {"id": "A4", "status": "pending",   "total": 449.00, "items": 12},
    ]
}

# Extract all order IDs
expr = parse("$.orders[*].id")
ids = [m.value for m in expr.find(data)]
# ['A1', 'A2', 'A3', 'A4']

# Filter: pending orders over $100
expr = parse("$.orders[?(@.status == 'pending' & @.total > 100)].id")
large_pending = [m.value for m in expr.find(data)]
# ['A2', 'A4']

# Access full match context: value, path, and parent
expr = parse("$.orders[*]")
for match in expr.find(data):
    print(f"Path: {match.full_path} | Status: {match.value['status']}")
# Path: orders.[0] | Status: shipped
# Path: orders.[1] | Status: pending

# In-place update: set all pending order statuses to 'processing'
expr = parse("$.orders[?(@.status == 'pending')].status")
expr.update(data, "processing")
# data["orders"][1]["status"] == "processing" ✓

Go: gjson (High-Throughput Extraction)

gjson — fast JSON path extraction, own syntax (not RFC 9535)
package main

import (
    "fmt"
    "github.com/tidwall/gjson"
)

const json = `{
    "users": [
        {"id": 1, "name": "Alice", "role": "admin", "score": 92},
        {"id": 2, "name": "Bob",   "role": "user",  "score": 74},
        {"id": 3, "name": "Charlie","role":"admin",  "score": 88}
    ]
}`

func main() {
    // gjson uses dot notation — no $ prefix, simpler than JSONPath
    name := gjson.Get(json, "users.0.name")
    fmt.Println(name.String()) // "Alice"

    // Array wildcard: #.field
    names := gjson.Get(json, "users.#.name")
    fmt.Println(names)         // ["Alice","Bob","Charlie"]

    // Filter with #[condition]# syntax (not RFC 9535)
    admins := gjson.Get(json, `users.#[role=="admin"]#`)
    admins.ForEach(func(_, v gjson.Result) bool {
        fmt.Println(v.Get("name").String())
        return true
    })
    // Alice, Charlie

    // GetMany: extract multiple paths in one pass (efficient for large JSON)
    results := gjson.GetMany(json, "users.0.name", "users.0.role", "users.#")
    fmt.Println(results[0], results[1], results[2])
    // "Alice" "admin" 3
}

// gjson is ~5-10× faster than encoding/json for extraction because
// it does not unmarshal the entire document. Trade-off: gjson uses
// its own path syntax, not RFC 9535.
//
// For RFC 9535-compatible JSONPath in Go: github.com/PaesslerAG/jsonpath

JSONPath Library Comparison

LibraryLanguageRFC 9535FiltersNotes
jsonpath-plusJS/NodePartialYes + extensionsMost popular npm package; adds type selectors and custom function support
jsonpath-rfc9535JS/Node100% CTSYes (strict)Full compliance test suite pass; smaller ecosystem but spec-correct
jsonpath-ngPythonTrackingYes (ext module)Supports in-place updates; actively maintained for RFC conformance
jmespathJS/Py/GoNo (different spec)Yes (different syntax)AWS CLI standard; stricter type system; better for data transformation
gjsonGoNo (own syntax)Yes (#[...]#)5–10× faster than encoding/json; 14k+ GitHub stars; own path format
JSONataJSNo (own spec)Yes (own syntax)Full transformation language; powers IBM App Connect and Node-RED

Practical recommendation: use jsonpath-plus in JavaScript for most projects — it has the largest community and handles ~90% of real-world JSONPath needs. Switch to jsonpath-rfc9535 when you need strict spec compliance or are writing test suites. In Python, jsonpath-ng is the best maintained option. In Go, gjson delivers the best performance with the trade-off of a non-standard path syntax.

JSONPath vs JMESPath vs jq

Three different query ecosystems serve different use cases. Picking the wrong one is a common source of friction:

ToolSpecPrimary UseRoot SyntaxTransforms
JSONPathRFC 9535App code, API testing, Kubernetes$.pathExtract only
JMESPathjmespath.orgAWS CLI, AWS SDKspath (no $)Projections, sort_by
jqjqlang.orgShell scripts, CI/CD pipelines.pathFull reshaping
JSONatajsonata.orgIBM App Connect, Node-REDpath (no $)Full expression language
Same query in four different syntaxes
// Goal: get names of all active users from a JSON document

// JSONPath (RFC 9535) — use in app code (Node.js, Python, Go)
$.users[?(@.active == true)].name

// JMESPath — use in AWS CLI or AWS SDK --query flag
users[?active == `true`].name

// jq — use in bash scripts and CI/CD
# echo "$JSON" | jq '[.users[] | select(.active == true) | .name]'

// JSONata — use in IBM App Connect / Node-RED flows
users[active = true].name

The key distinction: JSONPath and JMESPath extract. jq and JSONata transform. If you need to reshape data — create new keys, aggregate values, convert types — use jq in scripts or JSONata in transformation pipelines. If you just need to select values from a known structure, JSONPath is the right choice for embedded application use. See the REST API best practices guide for patterns on working with API response data.

Real-World JSONPath Patterns

Kubernetes: kubectl JSONPath Output

kubectl -o jsonpath — extract pod data without jq
# Get all pod names in a namespace
kubectl get pods -o jsonpath='{.items[*].metadata.name}'

# Get pods that are Running (filter by phase)
kubectl get pods -o jsonpath='{.items[?(@.status.phase=="Running")].metadata.name}'

# Get restart count for first container in a specific pod
kubectl get pod my-app-abc123 -o jsonpath='{.status.containerStatuses[0].restartCount}'

# Extract all container images across all pods (with namespace prefix)
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}: {range .spec.containers[*]}{.image} {end}{"
"}{end}'

# NOTE: kubectl uses a pre-RFC 9535 JSONPath variant.
# Filter expression syntax may differ from RFC 9535 in edge cases.

API Testing: Asserting Response Structure

JSONPath assertions in API test suites (TypeScript)
import { JSONPath } from 'jsonpath-plus'
import { test, expect } from 'vitest'

async function fetchOrders(): Promise<unknown> {
  const res = await fetch('/api/orders')
  return res.json()
}

test('all orders have valid status', async () => {
  const data = await fetchOrders()
  const statuses = JSONPath({ path: '$..status', json: data }) as string[]
  const valid = new Set(['pending', 'processing', 'shipped', 'delivered', 'cancelled'])

  const invalid = statuses.filter(s => !valid.has(s))
  expect(invalid).toHaveLength(0)
})

test('no single order exceeds business limit', async () => {
  const data = await fetchOrders()
  const totals = JSONPath({ path: '$.orders[*].total', json: data }) as number[]
  const oversized = totals.filter(t => t > 10_000)
  expect(oversized).toHaveLength(0)
})

test('every order has at least one line item', async () => {
  const data = await fetchOrders()
  // get count of items in each order
  const itemCounts = JSONPath({ path: '$.orders[*].items', json: data }) as unknown[][]
  const empty = itemCounts.filter(items => items.length === 0)
  expect(empty).toHaveLength(0)
})

For validating the shape of the JSON itself (required fields, type constraints, enum values), use JSON Schema rather than JSONPath — they complement each other. See the JSON Schema validation guide for a complete reference on draft 2020-12 and AJV integration.

Frequently Asked Questions

What is JSONPath?

JSONPath is a query language for extracting values from JSON documents, analogous to XPath for XML. Created by Stefan Gössner in a 2007 blog post and standardized as IETF RFC 9535 in February 2024, it defines a string syntax starting with $ (root) and using path segments (.name, [0], [*], ..) to select values. A single expression can return multiple matching values.

What does $ mean in JSONPath?

$ is the root node identifier — it represents the entire JSON document being queried. Every valid JSONPath expression starts with $. From there you append child selectors (.name), array indexes ([0]), wildcards ([*]), recursive descent (..name), or filter expressions ([?()]) to navigate to the values you need.

How do I use JSONPath in JavaScript?

Install jsonpath-plus with npm install jsonpath-plus. Then: import { JSONPath } from "jsonpath-plus"; const results = JSONPath({ path: "$.users[*].name", json: data }). Returns an array of matched values. Set wrap: false to get the first result as a scalar. For strict RFC 9535 compliance, use the jsonpath-rfc9535 package instead.

What does .. do in JSONPath?

The .. operator is recursive descent — it searches for matching nodes at any depth in the document. $..price returns all price values regardless of where they appear in nested objects or arrays. It is equivalent to XPath's // axis. Use it when you don't know the exact nesting depth of the data. Be aware that on large documents it can be slow since it traverses the entire tree.

What is the difference between JSONPath and JMESPath?

JSONPath (RFC 9535) uses $ prefix, .. for recursive descent, and [?(@.field)] filter syntax. JMESPath uses no $ prefix, [] for wildcard projections, and ? filters without @. JMESPath is the AWS CLI standard with stronger transformation capabilities (sort_by, max_by, projections) and a stricter type system. Choose JSONPath for general-purpose JSON querying in application code; JMESPath for AWS integrations.

Is JSONPath the same as JSON Pointer (RFC 6901)?

No. JSON Pointer (RFC 6901) addresses exactly one location using slash-delimited paths like /users/0/name. It always resolves to a single value or fails. JSONPath can return multiple values, supports wildcards, recursive descent, and filter expressions. JSON Pointer is used in JSON Patch (RFC 6902) and JSON Schema $ref. JSONPath is used for querying and extraction.

How do I filter JSONPath results by a value?

Use filter expressions [?()]. Examples: $[?(@.price < 10)] for price under 10, $[?(@.status == "active")] for string equality, $[?(@.isbn)] to check key existence, $[?(@.active == true && @.role == "admin")] for combined conditions. The @ symbol refers to the current node. RFC 9535 built-in functions also work in filters: $[?(length(@.name) > 5)] filters by string length.

Test JSONPath Expressions Live

Paste your JSON and write JSONPath expressions to extract exactly the data you need. Instant results with syntax highlighting. Runs entirely in your browser.

Open JSON Path Tester →