AppCrib
Developer Tools

JSON Query Languages Compared: jq, JSONPath, JMESPath, and When Each Is the Right Pick

Domain knowledge·Published by AppCrib··
JqbinThe jq playground developers actually keep open.

The first time you run kubectl get pods -o jsonpath='{.items[*].metadata.name}', then try the same selection with jq '.items[].metadata.name', the outputs look different. JSONPath returns a space-separated string. jq returns a stream of quoted JSON values, one per line. Both are described as "JSON query languages." Both pull names out of an array. The shape of what comes back is different anyway, and the difference matters as soon as you pipe it anywhere.

The part introductory guides skip: jq, JSONPath, and JMESPath are not the same thing in three different uniforms. They share the same problem space, which is querying JSON, and they overlap on the easy cases. The disagreements show up in projection, filtering, and anything more elaborate than a flat selection. Each was designed under different assumptions about what JSON querying should feel like, and those assumptions shape what you can and can't do without falling back to shell glue.

Three Tools, Three Mental Models

JSONPath is a path-expression language. You write something that looks like a structured selector, and the engine returns matching values. The expressive ceiling is high enough for most extractions but stops short of real transformation.

JMESPath is a projection language. It borrows the path-expression idea from JSONPath but commits harder to operations, with built-in functions for filtering, slicing, and reshaping. It's the language behind --query in the AWS CLI, and that pedigree shows. The whole design points at terminals and config files where the input is API output and the goal is a single value or a short list.

jq is a functional programming language that happens to take JSON as input and emit JSON as output, the way awk is a programming language that happens to read text. You can write conditionals, define functions, recurse, fold, and chain. The grammar lets you build things JSONPath can't express at all.

JSONPath: Seventeen Years Between Proposal and Standard

Stefan Goessner sketched JSONPath in 2007 as a JSON analog to XPath. It spread fast because it was small enough to implement in a weekend and useful enough to embed in libraries. Every implementation diverged on the edges. There were Python forks, JavaScript forks, Java forks, and a half-dozen "JSONPath-Plus" variants that each fixed different gaps.

That mess persisted for seventeen years. The IETF finally produced RFC 9535 in February 2024, retroactively standardizing what the implementations had been doing inconsistently. RFC 9535 defines $ as the root, * as the wildcard, .. as descendant traversal, and [?expr] as a filter selector. The same vocabulary Goessner used, with the holes filled in.

The catch: most JSONPath libraries in the wild predate RFC 9535. Even now, kubectl's JSONPath is a different dialect from jayway/JsonPath in Java, which is different again from what most JavaScript libraries ship. Reading "use JSONPath" in documentation tells you which family of expression to write. It doesn't tell you which dialect. You usually have to read the library's docs to find out which behaviors are off-spec.

JMESPath: Amazon's Answer to Projection

JMESPath was developed at Amazon by James Saryerwinnie and first published publicly around 2014. It powers the --query flag in the AWS CLI, Ansible's json_query filter, and the boto3 paginators. If you've ever written aws ec2 describe-instances --query 'Reservations[*].Instances[].InstanceId', you've written JMESPath.

The JMESPath spec lives at jmespath.org and is rigorously defined. There's a compliance test suite, and most implementations actually pass it. That's a meaningful difference from JSONPath's first decade. When you write a JMESPath expression, it does the same thing on every supported runtime. The cost of that consistency is scope. JMESPath is deliberately narrower than jq. It can filter, project, slice, and call a built-in function. It can't define a new function, can't recurse user-defined logic, and doesn't let you build up a result by accumulation.

For API extraction work, which is what Amazon optimized for, the constraint is the point. Most CLI tooling wants a one-liner that pulls one value or one column out of structured output. JMESPath is what you get when you remove every feature that doesn't help with that job.

jq: A Functional Language That Happens to Read JSON

Stephen Dolan released jq in October 2012 and described it from the start as "like sed for JSON." That undersells it. Within a couple of versions, jq grew variables, function definitions, conditionals, error handling, and a streaming evaluation model where every expression produces a sequence of values. You can write def fact($n): if $n <= 1 then 1 else $n * fact($n - 1) end; fact(5) and jq evaluates it. None of the other JSON query tools can do that.

The streaming model is the part that surprises people. In jq, .[] doesn't return an array. It returns each element separately, one after another. If you want them back in an array, you wrap the expression in brackets: [.[]]. Every operator in the language obeys this, including the comma operator, which is the language's way of producing two streams from one expression. That mental model is closer to Prolog or Haskell than to XPath.

jq 1.8 shipped in mid-2025 after a long stretch where the language sat at 1.6 with no releases. The jqlang community took over maintenance from Stedolan around 2022. The releases since (1.7 in mid-2023, 1.7.1 later that year, and 1.8 in 2025) added SQL-style operators, better regex semantics, and lambda parameters that match what the Rust port (jaq) had been doing for years. The point: jq is still moving, even after a decade.

Where the Same Task Produces Different Answers

TaskJSONPath (RFC 9535)JMESPathjq
Get all name fields from an array of objects$.items[*].nameitems[].name.items[].name
Filter array by string equality$.items[?(@.status=='active')]items[?status=='active']`.items[] \select(.status=="active")`
Return an array (not a stream)Native: every result is an arrayNative: returns a listMust wrap: [.items[]]
Compute a derived value (sum, count)Not supportedLimited (sum, length, avg)Native: reduce, map, arbitrary arithmetic
Define a reusable functionNot supportedNot supporteddef f: ...; f
Standardized specRFC 9535 (Feb 2024)jmespath.org spec + compliance suiteReference manual, no formal standard

The "return a stream vs return an array" row is the one that bites most often. A JSONPath query for $.items[*].name returns ["alice", "bob"], already wrapped in an array. The equivalent jq expression .items[].name returns:

"alice"
"bob"

Two separate JSON outputs, one per line. Pipe that into another shell tool and you'll wonder why your array became a string. Wrap the expression in brackets and it becomes an array. Leave it bare and it's a stream of values. JSONPath libraries don't make you think about this, because they always materialize the result. jq makes you think about it on every query, and the manual is opinionated about why: the streaming model is what lets the language compose without intermediate allocations.

Which One to Reach For

The honest answer is "whichever your ecosystem already speaks." If you live in kubectl and helm, you'll use JSONPath whether you like it or not. If you live in the AWS CLI, you'll write JMESPath, and resisting it costs more than learning it. jq earns its place when the task outgrows what those two can express: transformations, joins across keys, computed fields, anything that needs more than "pull this value out."

A useful rule of thumb. If your query has a single wildcard or a single filter and nothing else, any of the three will do. If you need to reshape the output by combining two arrays, adding a derived field, or accumulating a total, reach for jq. If you're writing a one-liner to drop into a Makefile or a CI script and the input is AWS API output, JMESPath is the path of least friction. The mistake is treating them as interchangeable. That's how you end up debugging why a JSONPath filter returned an empty array on a payload jq parses cleanly.

The disagreement isn't a problem to fix. It's a consequence of three different teams deciding what "querying JSON" should mean. JSONPath optimized for path expressions, JMESPath for predictable projection, jq for transformation. Each is the right answer for a different question.

If you want to feel the difference rather than read about it, Jqbin runs real jq 1.8 in your browser with shareable links, so you can put a JSONPath or JMESPath expression next to its jq equivalent and watch how the outputs diverge on the same input.

Jqbin
The jq playground developers actually keep open.
Try Jqbin