JMESPath for Beginners: Projection Queries That Beat JSONPath
Bottom line: JMESPath reshapes batches of objects more directly than JSONPath. This beginner guide covers syntax, projections, pipes, built-in functions and AWS CLI usage.

Bottom line: use JSON Pointer to locate by position, JSONPath to filter or recurse, and JMESPath when you need to reshape the result or project it on the command line; a single known field needs neither.
If your goal is to reshape a batch of objects into a different shape, JMESPath is often more direct than JSONPath. It does not look for nodes; it constructs a result.
1. What JMESPath Is
JMESPath is a query language designed specifically for JSON. It was originally created for filtering AWS CLI output and later spread to many languages through implementations such as jmespath.js, jmespath.py, and go-jmespath. Its goal differs from JSONPath:
- JSONPath focuses on selecting a set of nodes by path and condition, and returns a node set;
- JMESPath focuses on transforming the input JSON and returns a value of any shape.
This difference makes JMESPath especially good at list projections, field renaming, and result trimming, such as reducing twenty fields from an API response to the three fields the frontend actually needs. If you have not read the basics of JSONPath yet, start with JSONPath Query Language in Practice.
2. Basic Syntax: From Identifiers to Indices
JMESPath uses fewer symbols than JSONPath and reads more like an extraction expression:
| Syntax | Meaning | Example |
|---|---|---|
name |
Access an object member | people |
a.b |
Access nested members | store.bicycle.color |
[0] |
Access an array index | people[0] |
[-1] |
Access the last element | people[-1] |
[0:2] |
Slice, inclusive start and exclusive end | people[0:2] |
[*] |
List projection | people[*].name |
[] |
Flatten projection | people[].tags[] |
[?condition] |
Filter projection | people[?age > 18] |
{key: expr} |
Multi-select hash | {name: name, age: age} |
[expr, expr] |
Multi-select list | [name, age] |
| |
Pipe the left result into the right side | people[*].age | [0] |
A minimal example:
{
"people": [
{ "name": "Alice", "age": 31, "city": "Shanghai" },
{ "name": "Bob", "age": 17, "city": "Beijing" }
]
}
people[0].name → "Alice"
people[-1].city → "Beijing"
people[0:2].name → ["Alice", "Bob"]
Note why people[0:2].name returns an array: a slice returns a list, and the following .name is automatically projected over every element in that list. This is the biggest difference between JMESPath and ordinary object access.
3. Projections: Turning One List into Another
A projection is the core concept in JMESPath. It works in two steps: evaluate every element of a list, then collect the results into a new list. Elements that evaluate to null are dropped automatically.
people[*].name // ["Alice", "Bob"]
people[*].{name: name, age: age} // a list of objects
people[?age > 18].name // ["Alice"]
The second line is a typical projection expression: people[*].{name: name, age: age}. It rebuilds every element as { "name": ..., "age": ... } and discards the extra city field. Compared with JSONPath, which gives you nodes that you then map in code, this saves a loop.
Two kinds of projections are worth distinguishing:
[*]preserves the original list structure and skips null values;[]is a flatten projection that first collapses one level of nested lists, which suits expressions such aspeople[].tags[]that need "all tags of all people".
4. Pipes and Multi-Select
The pipe | means "stop the current projection and treat the result as a new input". Its most common use is taking an index after a projection:
people[*].name | [0] → "Alice"
people[*].age | sort(@) | [-1] → the maximum age
Without the pipe, people[*].name[0] is interpreted as taking the first character of every name, which is not what you want. The pipe is the most easily overlooked and most useful symbol in JMESPath.
There are two forms of multi-select:
// Multi-select hash: the result is an object
people[0].{name: name, age: age}
// Multi-select list: the result is an array
people[0].[name, age]
Multi-select hashes are good for constructing named results, while multi-select lists are good for positional consumption. Both can be nested inside projections, but note: in a multi-select hash, a key whose value is null is still kept, whereas a projection drops the entire null element.
5. Built-in Functions
The JMESPath standard defines a set of functions. Common ones include:
| Function | Purpose | Example |
|---|---|---|
length() |
Get the length | length(people) |
sort() |
Sort values | sort(people[*].age) |
sort_by() |
Sort by a field | sort_by(people, &age) |
max() / min() |
Maximum / minimum | max(people[*].age) |
sum() |
Sum values | sum(people[*].age) |
keys() |
Get object keys | keys(people[0]) |
contains() |
Check containment | contains(name, "Ali") |
to_string() |
Convert to string | to_string(age) |
The second argument of sort_by is an & expression, meaning "take this field from each element":
sort_by(people, &age)[*].name
These functions let JMESPath perform simple aggregation, whereas JSONPath is designed only for selection; summing and grouping usually require jq or code.
6. A Side-by-Side Comparison with JSONPath
| Dimension | JSONPath | JMESPath |
|---|---|---|
| Primary goal | Select nodes | Transform data |
| Root symbol | $ |
None; write field names directly |
| Current node | @ |
@ (only in function arguments) |
| Filter | [?(@.age > 18)] |
[?age > 18] |
| Wildcard | [*] |
[*] or [] |
| Construct new objects | Not supported | {name: name} |
| Pipes | Not supported | | |
| Standard | RFC 9535 | JMESPath Specification |
A one-sentence decision guide:
- Use JSONPath when you only need to find nodes that match a condition;
- Use JMESPath when you need to reshape the result into what the frontend expects;
- Use neither when you are reading one known field; just access the property directly.
If the query result must be written back to the document, neither language is appropriate. Use JSON Pointer and patches instead; see JSON Pointer, Patch, and Merge Patch.
7. JMESPath in AWS CLI
The --query parameter of the AWS CLI is JMESPath, which is its most widespread use case:
aws ec2 describe-instances \
--query 'Reservations[].Instances[].{Id: InstanceId, State: State.Name, Type: InstanceType}' \
--output table
This command does three things: it flattens two nested lists with [], keeps only three fields with a multi-select hash, and renders the result as a table with --output table. With JSONPath you would need more levels and would not get the "rename plus trim" effect.
Other common forms:
# Only running instances
aws ec2 describe-instances --query 'Reservations[].Instances[?State.Name==`running`].[InstanceId,PublicIpAddress]'
# The value of the Name tag
aws ec2 describe-instances --query 'Reservations[].Instances[].Tags[?Key==`Name`].Value[]'
Note that backticks enclose literals, not strings. In the AWS CLI, strings use single quotes and JSON literals use backticks. This difference is one of the most common beginner mistakes.
8. Common Mistakes
- Mixing up
[*]and[]: the former preserves nesting, the latter flattens one level, and the resulting structures are completely different; - Forgetting the pipe:
people[*].name[0]takes the first character of each name, not the first name; - Writing
@inside a filter: JMESPath filters use field names directly, so[?@.age > 18]is wrong; - Treating
nullas an empty string: projections dropnull, but multi-select hashes keep the key; - Using double quotes around literals in the AWS CLI: the shell expands them first and rewrites the expression.
Debugging advice: validate an expression on the smallest possible input before applying it to real data, and add --output json in the AWS CLI to see raw results. For everyday JSON editing and formatting, pairing with JSON Editors and Command-Line Tools makes locating field paths much faster.
FAQ
Can JMESPath and JSONPath be used together?
Yes, but it is usually unnecessary. A common approach is to trim a small object with JMESPath, then use ordinary property access in code. If you really need to locate by path and then modify, JSONPath reads and JSON Pointer writes, which is a clearer division of labor.
Why does people[*].name return something other than a single string?
Because [*] is a projection, and the result is always a list, even when there is only one person. To get a single value, add a pipe and an index: people[*].name | [0].
What happens to null inside a projection?
A list projection automatically skips elements that evaluate to null, so the result may be shorter than the original list. A multi-select hash keeps the key and preserves the null value, which matters when constructing API responses.
Can JMESPath do aggregation and grouping?
It can do basic aggregation with sum(), max(), and sort_by(). Grouping, joins, and window functions are outside the standard. Leave complex analysis to jq, SQL, or code.
How do I decide between JMESPath and JSONPath?
Use JSONPath when you need to select nodes by path and condition and receive a node set; use JMESPath when you need to reshape the result into what the frontend expects, with field renaming and trimming. When you are reading one known field, use neither; just access the property.
Should I use a pipe to take the first projected value?
Yes. people[*].name is a projection, so the result is always a list; without a pipe, people[*].name[0] takes the first character of every name. Write people[*].name | [0] to get the first value.