jq vs JSONPath vs JMESPath: Choosing the Right JSON Query Tool

Three tools, three names that all sound like "the thing that queries JSON," and zero shared syntax between them. That is the trap. You learn jq for your shell pipelines, then try to paste a jq filter into a kubectl -o jsonpath= flag, and nothing works. You write a JSONPath expression that runs fine in your test harness, then feed it to aws ... --query and get an error, because the AWS CLI does not speak JSONPath at all.

jq, JSONPath, and JMESPath are not three dialects of one language. They are three separate languages with different design goals, different operators, and different homes. Picking the right one is mostly about picking the right tool you are feeding it into, not personal preference. This guide draws the lines so you stop cross-wiring them.

The fastest way to internalize the difference is to run the same query in each. You can do that for jq right now in the jq playground, which runs the genuine jq processor compiled to WebAssembly in your browser, and for the other two in the linked testers below.

The real first question: are you extracting or transforming?

Before you compare syntax, split the problem in two. Most of the confusion comes from lumping all three under "JSON query tool" when they don't do the same job.

  • Extraction means: pull a value or a list of values out of a document and hand them off. You're reaching in and grabbing. JSONPath and JMESPath are extraction languages. They were built to select.
  • Transformation means: reshape the data. Rename keys, compute a sum, group by a field, splice two arrays together, emit CSV. jq does this. It is a stream processor that happens to start with selection, not a selection tool that happens to transform.

This single distinction settles most decisions. If all you need is "give me the names of the running pods," any of the three can do it. The moment you need "give me the names, but uppercased, joined with commas, only for pods older than a day," you are out of JSONPath and JMESPath territory and into jq. JMESPath has some reshaping (multiselect hashes, functions, pipes), but it stops well short of jq's full programming model.

Keep that fork in mind as you read the rest. Syntax differences are real, but the job each language was designed for is the thing that should drive your choice.

One query, three languages

Here is the same task written three ways. Task: from a document with a locations array of objects, return the name of every location where state equals WA.

LanguageExpression
jq.locations[] | select(.state == "WA") | .name
JSONPath$.locations[?(@.state=='WA')].name
JMESPathlocations[?state=='WA'].name

Look at what changes, because none of it is cosmetic:

  • The root. jq starts paths with a leading dot (.locations). JSONPath starts with $ for the document root. JMESPath uses a bare identifier with no prefix at all.
  • String quoting. jq uses double quotes ("WA"). JSONPath and JMESPath both use single quotes ('WA'). Swap them and you get a parse error or a silent miss.
  • Equality. jq writes == inside an explicit select(...). JSONPath buries the filter in [?(...)] with parentheses. JMESPath uses [?...] with no parentheses.
  • Iteration. jq makes the stream explicit: .locations[] emits each element, then pipes flow downstream. The other two iterate implicitly inside the bracket filter.

The detail that proves these are different languages and not dialects: the @ symbol means two different things. In JSONPath, @ is the current node being tested inside a filter, which is why the filter reads @.state. In JMESPath, @ is the current node being evaluated, used in functions like sort(@). Same character, different role, and no mechanical find-and-replace converts one expression to the other.

jq: the shell transformer (and the tool to actually run)

jq is a command-line JSON processor and a small functional language. It reads a stream of JSON values, runs each through a filter program, and writes a stream of JSON values out. The whole thing is built around the pipe, exactly like Unix tools, which is why it feels native in a shell.

jq is what you reach for when JSON is moving through a terminal or a script: massaging a curl response, reshaping a config file, turning an API payload into CSV with @csv, or extracting a field for a downstream command. It has variables, conditionals, reduce, group_by, string interpolation, and arithmetic. None of that exists in JSONPath, and only a slice of it exists in JMESPath.

That power is also the trade. A jq filter that does real reshaping is its own little program, and the syntax is unforgiving about quoting and stream semantics. The fastest way to get it right is to iterate against real data. The jq playground runs jq compiled to WebAssembly, so pipes, select, map, group_by, @csv, and the standard flags (-r, -c, -s, -S, -n) behave as they do in your terminal. It executes locally in the browser, so you can paste a payload with secrets in it and nothing leaves the page. Get the filter passing there, then copy it into your script verbatim.

# raw strings, one name per line
jq -r '.locations[] | select(.state == "WA") | .name' data.json

# reshape into CSV rows
jq -r '.locations | map([.name, .state]) | .[] | @csv' data.json

Do not try to use jq inside kubectl or the AWS CLI. It is not embedded in either. jq is something you pipe into, after the tool has produced JSON.

JSONPath: extraction, kubectl's home, and the standard nobody fully agreed on

JSONPath is the oldest of the three as a widely-used idea. It began in 2007 as a blog post by Stefan Goessner, and for years it was a convention rather than a specification. That history matters, because it left a mess.

JSONPath's natural home today is kubectl. When you write kubectl get pods -o jsonpath='{.items[*].metadata.name}', that is JSONPath, not JMESPath. It is the right tool for reaching into the predictable, deeply-nested structure of Kubernetes objects and pulling out a field or a list. It is extraction-only by design: it selects nodes, it does not reshape them. kubectl's flavor is actually a superset, with its own range/end keywords layered on top.

The fragmentation caveat is the part that bites people. JSONPath was finally standardized as RFC 9535 in February 2024, but most implementations predate the RFC and diverge from it and from each other. kubectl's JSONPath, the various library implementations across languages, and the RFC do not all agree on filter syntax, recursive descent edge cases, or how missing keys behave. There is no single canonical JSONPath the way there is a single jq binary. An expression that works in one engine can fail or return different results in another.

The practical consequence: test JSONPath against the exact engine you will run it in, and don't assume portability. To prototype expressions, use the JSONPath tester, then sanity-check anything kubectl-bound against kubectl itself, since its dialect is its own.

$.locations[*].name              # every name
$.locations[?(@.state=='WA')]    # objects where state is WA
$..name                          # recursive: every name, any depth

JMESPath: the AWS CLI --query language

JMESPath is the youngest of the three and the most disciplined. It has a formal specification and an official compliance test suite, which means implementations across languages actually behave the same way. After JSONPath's fragmentation, that consistency is the headline feature.

Its home is the cloud tooling ecosystem. The AWS CLI --query flag is JMESPath. So is boto3's paginator and waiter filtering, the Azure CLI's --query flag, and Ansible's json_query filter. If you are slicing API responses inside any of those, you are writing JMESPath whether you call it that or not.

JMESPath sits between the other two on the extract-versus-transform axis. It is more than pure selection: it has multiselect lists and hashes for reshaping output, pipe expressions, functions like sort, length, and contains, and projections. But it is not jq. There is no arithmetic over arbitrary fields, no reduce, no general-purpose programming. It is extraction with a reshaping layer on top, tuned for picking apart cloud API responses.

# AWS CLI: just the instance IDs
aws ec2 describe-instances \
  --query 'Reservations[].Instances[].InstanceId'

# filter, then sort
locations[?state=='WA'].name | sort(@)

That last line is the WA filter from earlier, with a JMESPath pipe sorting the result. Prototype expressions in the JMESPath tester before you wire them into a --query flag, since CLI quoting and shell escaping pile on enough friction without also debugging the expression itself.

The decision guide

Reach for each one by where the JSON is going, not by which syntax you like:

  • Use jq when JSON is flowing through a shell or a script and you need to transform it: reshape, compute, group, join, or emit CSV. It is also the most capable for pure extraction, so if you live in the terminal and don't need portability, jq alone covers most needs.
  • Use JSONPath when your tool already speaks it, above all kubectl's -o jsonpath=. Use it for extraction from predictable structures. Pin your expressions to the specific engine and don't trust them to port, because the dialects diverge.
  • Use JMESPath when you are inside the AWS CLI, Azure CLI, boto3, or Ansible. The --query flag leaves you no choice, and that is fine, because JMESPath's formal spec makes it the most predictable of the three.

Two facts to tattoo on the back of your hand. First: kubectl is JSONPath, the AWS CLI is JMESPath, and the two are not interchangeable. Second: jq is something you pipe into after the fact, never something you embed in either CLI. Get those straight and the cross-wiring errors disappear.

Use the tool

Skip the manual work. The companion tool runs this in your browser, with nothing uploaded.

jq Playground

Frequently asked questions

Can I use a jq filter inside kubectl?

No. kubectl's -o jsonpath= flag uses JSONPath, which shares no syntax with jq. jq is a separate command-line processor. The usual pattern is to let kubectl output JSON with -o json, then pipe that into jq: kubectl get pods -o json | jq '...'. jq is never embedded in kubectl itself.

Does the AWS CLI support JSONPath?

No. The AWS CLI --query flag uses JMESPath, not JSONPath. They look similar but differ: JSONPath expressions start with $ and put filters in [?(...)], while JMESPath uses bare identifiers and [?...] without parentheses. A JSONPath expression pasted into --query will usually fail or return nothing.

Is JSONPath an official standard?

It was standardized as RFC 9535 in February 2024, but most implementations predate the RFC and diverge from it and from each other. kubectl's JSONPath, library implementations, and the RFC do not fully agree on filter and edge-case behavior, so there is no single canonical JSONPath. Always test against the exact engine you will run in.

What's the difference between @ in JSONPath and JMESPath?

In JSONPath, @ refers to the current node being evaluated inside a filter, as in [?(@.state=='WA')]. In JMESPath, @ refers to the current node being evaluated in the expression as a whole, used with functions like sort(@). Same character, different role in each language, and no mechanical conversion between the two.

Where does JSONata fit in?

JSONata is a fourth, separate language, used mainly in low-code platforms and integration tools like Node-RED. Like jq it can transform and compute, not just extract, but it has its own syntax distinct from all three above. If you are not in a tool that specifically supports JSONata, you almost certainly want jq, JSONPath, or JMESPath instead. You can experiment with it in the JSONata tester.

Which should I learn first?

jq, if you spend time in a terminal. It is the most generally useful and the only one that handles real transformation, so the effort pays off across the widest range of tasks. Learn JSONPath or JMESPath when a specific tool forces your hand, namely kubectl for JSONPath and the AWS or Azure CLI for JMESPath.

Related tools