# JMESPath Query

> Filter, project and reshape JSON with JMESPath expressions, with error positions.

Category: Text & Data · Page: https://util.caruana.me/tools/json-query/

## API, MCP and A2A

### Query JSON with JMESPath (`json_query`)

`GET|POST https://util.caruana.me/api/json/query` · MCP tool `json_query` · A2A skill `json_query` · [reference](https://util.caruana.me/reference/json_query/)

Evaluates `expression` against `input` with @jmespath-community/jmespath (JMESPath Community Edition): the original JMESPath language plus `let` expressions, `$` for the root, arithmetic (`+ - * / % //`), the ternary `? :` operator, and extra functions such as `items`, `from_items`, `group_by`, `zip`, `find_first`, `pad_left`, `split`, `replace`, `trim`. A path that does not exist gives `null` (not an error). Syntax errors give the column of the offending token; unknown functions are reported before evaluation; type errors (for example `length` of a number) are reported with the function position.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `input` | string (≤ 500000 chars) | yes | The JSON document, as JSON text (string field or raw `text/plain` POST body). |
| `expression` | string (≤ 2048 chars) | yes | The JMESPath expression, e.g. `sort_by(items, &price)[-1].name`. JSON literals are written between backtick characters; raw strings go in single quotes. |
| `indent` | 2 \| 4 \| tab \| none | no | JSON output indentation: `2` or `4` spaces, `tab`, or `none` for minified output (no whitespace at all). |

Privacy: The server receives the JSON document and the expression, processes it in memory inside the request, and returns the result. Your input is not stored or logged; only the per-call metadata row (operation, time, status, sizes) is recorded. The server makes no network requests. For confidential documents use the page, which does the same work in your browser and sends nothing.


## User guide

The tool sits in a window that works like a desktop app: menus on the top row and the **full-screen** icon at its right (**Esc** leaves full screen). The set-up options described below are in **Settings → Options…**, an overlay, and the **Settings** menu also lists the common choices directly. To keep the options in view instead, choose **View → Inline config**; the page remembers that choice. **Help → Keyboard shortcuts…** lists the keys.

1. **Paste JSON** into the left box (or click **Sample**).
2. **Type a JMESPath expression** above it. The result and its type appear on the right as you type. The buttons next to the expression load common patterns.
3. If the expression is wrong, the error shows the column and the expression with a caret under the problem. If the JSON is wrong, **Go to error** selects the bad character.

A path that does not exist is not an error: it gives `null`. A filter that matches nothing gives `[]`.

**Three things that trip people up.**

- **Literals need backticks.** `` price > `10` `` compares with the number 10; `price > 10` is a syntax error. Strings can be raw literals in single quotes: `name == 'bread'`. A backtick literal must be valid JSON: `` `"text"` ``, `` `[1, 2]` ``, `` `true` ``.
- **Projections keep going until a pipe.** `items[*].name[0]` applies `[0]` to each name (a string, so each gives null and is dropped). To take the first element of the list of names, stop the projection with a pipe: `items[*].name | [0]`.
- **Ordering comparisons are for numbers.** `<`, `<=`, `>`, `>=` between strings give `null`, and a filter treats `null` as false.

## Specification

**Dialect.** JMESPath Community Edition, as implemented by [@jmespath-community/jmespath](https://github.com/jmespath-community/typescript-jmespath) 1.3 (a tree-walking interpreter: no code generation). It is the original [JMESPath](https://jmespath.org/specification.html) language (identifiers, sub-expressions, index and slice expressions, wildcard and flatten projections, filters, multi-select lists and hashes, pipes, `||`, `&&`, `!`, comparisons, functions, expression references with `&`) plus these Community extensions:

| Extension                                                                 | Example                                             |
| ------------------------------------------------------------------------- | --------------------------------------------------- |
| Root reference `$` (the whole document, even inside a projection)         | `items[*].[name, $.owner.name]`                     |
| `let` expressions with `$variables`                                       | ``let $limit = `3` in items[?price > $limit].name`` |
| Arithmetic: `+`, `-` (also unary), `*`, `/`, `%`, `//` (integer division) | `` items[2].price * `10` ``                         |
| Ternary `condition ? a : b` (experimental in the library)                 | ``length(items) > `2` ? 'many' : 'few'``            |
| Raw string literals in single quotes                                      | `'fresh'`                                           |

**Functions.** `abs`, `avg`, `ceil`, `contains`, `ends_with`, `find_first`, `find_last`, `floor`, `from_items`, `group_by`, `items`, `join`, `keys`, `length`, `lower`, `map`, `max`, `max_by`, `merge`, `min`, `min_by`, `not_null`, `pad_left`, `pad_right`, `replace`, `reverse`, `sort`, `sort_by`, `split`, `starts_with`, `sum`, `to_array`, `to_number`, `to_string`, `trim`, `trim_left`, `trim_right`, `type`, `upper`, `values`, `zip`. No custom functions are registered. Calling any other name is reported before evaluation, with its position.

**Errors.**

- **Syntax errors** give the column of the token where the problem was detected; an incomplete expression points just past the end. The library's own messages have no position, so the position is found by re-parsing prefixes of the expression and taking the first one that fails the same way. Unterminated quoted identifiers and invalid backtick literals point at their opening quote or backtick. (The library accepts an unterminated raw string such as `'abc` as if it were closed.)
- **Evaluation errors** (a function given the wrong type, such as `length` of a number; division by zero; a slice step of 0) are reported with the position of the function involved when it can be identified.
- **JSON errors** give line, column, offset and a JSON Pointer, from the same strict RFC 8259 parser as the JSON tool. Duplicate keys and integers beyond 2^53 − 1 are reported as warnings; the query sees the last value of a duplicate key, and numbers as IEEE 754 doubles.

**Result.** `result` is the value; `type` is its JSON type (`object`, `array`, `string`, `number`, `boolean` or `null`); the formatted text uses 2 or 4 spaces or no whitespace. Object keys that look like array indexes (`"2"`, `"10"`) come first, in numeric order, in any object the query returns: this is how JavaScript orders object keys. Inherited JavaScript property names are never visible: `constructor` on `{}` is `null`.

**Examples.** All results below are produced by this tool against this document:

```json
{
  "items": [
    { "name": "apple", "price": 1.2, "tags": ["fruit"], "stock": { "qty": 0 } },
    { "name": "bread", "price": 3.5, "tags": ["bakery", "fresh"], "stock": { "qty": 12 } },
    { "name": "cheese", "price": 12, "tags": ["dairy"], "stock": { "qty": 4 } }
  ],
  "owner": { "name": "shop", "email": "shop@example.com" }
}
```

| What                        | Expression                                                      | Result                                                                           |
| --------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Field of the first element  | `items[0].name`                                                 | `"apple"`                                                                        |
| Negative index              | `items[-1].name`                                                | `"cheese"`                                                                       |
| Projection                  | `items[*].name`                                                 | `["apple","bread","cheese"]`                                                     |
| Flatten nested lists        | `items[].tags[]`                                                | `["fruit","bakery","fresh","dairy"]`                                             |
| Filter by number            | ``items[?price > `3`].name``                                    | `["bread","cheese"]`                                                             |
| Filter by equality          | ``items[?stock.qty == `0`].name``                               | `["apple"]`                                                                      |
| Filter with a function      | `items[?contains(tags, 'fresh')].name`                          | `["bread"]`                                                                      |
| Combined conditions         | ``items[?price < `5` && stock.qty > `0`].name``                 | `["bread"]`                                                                      |
| Negation                    | ``items[?!(stock.qty > `0`)].name``                             | `["apple"]`                                                                      |
| Nested filter               | `items[?tags[?@ == 'fresh']].name`                              | `["bread"]`                                                                      |
| Multi-select list           | `items[*].[name, price]`                                        | `[["apple",1.2],["bread",3.5],["cheese",12]]`                                    |
| Multi-select hash (reshape) | `items[*].{name: name, qty: stock.qty}`                         | `[{"name":"apple","qty":0},{"name":"bread","qty":12},{"name":"cheese","qty":4}]` |
| Slice                       | `items[:2].name`                                                | `["apple","bread"]`                                                              |
| Reverse slice               | `items[::-1].name`                                              | `["cheese","bread","apple"]`                                                     |
| Sort by a field             | `sort_by(items, &price)[*].name`                                | `["apple","bread","cheese"]`                                                     |
| Largest by a field          | `reverse(sort_by(items, &stock.qty))[0].name`                   | `"bread"`                                                                        |
| `max_by` / `min_by`         | `max_by(items, &price).name`                                    | `"cheese"`                                                                       |
| Count                       | `length(items)`                                                 | `3`                                                                              |
| Count matches               | ``length(items[?stock.qty > `0`])``                             | `2`                                                                              |
| Sum                         | `sum(items[].stock.qty)`                                        | `16`                                                                             |
| Maximum                     | `max(items[].price)`                                            | `12`                                                                             |
| Summary object              | `{count: length(items), qty: sum(items[].stock.qty)}`           | `{"count":3,"qty":16}`                                                           |
| Object keys                 | `keys(owner)`                                                   | `["name","email"]`                                                               |
| Missing path                | `items[0].missing`                                              | `null`                                                                           |
| No matches                  | ``items[?price > `100`]``                                       | `[]`                                                                             |
| Default value               | `owner.phone \|\| 'none'`                                       | `"none"`                                                                         |
| First non-null              | `not_null(owner.phone, owner.email)`                            | `"shop@example.com"`                                                             |
| Pipe                        | `items \| [0].name`                                             | `"apple"`                                                                        |
| Projection continues        | `items[*].name[0]`                                              | `[]`                                                                             |
| Pipe stops the projection   | `items[*].name \| [0]`                                          | `"apple"`                                                                        |
| Join strings                | `join(', ', items[*].name)`                                     | `"apple, bread, cheese"`                                                         |
| Membership                  | `contains(items[*].name, 'bread')`                              | `true`                                                                           |
| String functions            | `split(owner.email, '@')[1]`                                    | `"example.com"`                                                                  |
| Map an expression           | `map(&upper(name), items)`                                      | `["APPLE","BREAD","CHEESE"]`                                                     |
| Object to pairs             | `items(owner)`                                                  | `[["name","shop"],["email","shop@example.com"]]`                                 |
| Pairs to object             | `from_items(items[*].[name, price])`                            | `{"apple":1.2,"bread":3.5,"cheese":12}`                                          |
| Group                       | ``group_by(items, &to_string(stock.qty > `0`))."true"[*].name`` | `["bread","cheese"]`                                                             |
| `let` variable              | ``let $limit = `3` in items[?price > $limit].name``             | `["bread","cheese"]`                                                             |
| Root reference              | `items[*].[name, $.owner.name]`                                 | `[["apple","shop"],["bread","shop"],["cheese","shop"]]`                          |
| Arithmetic                  | `` items[2].price * `10` ``                                     | `120`                                                                            |
| Ternary                     | ``length(items) > `2` ? 'many' : 'few'``                        | `"many"`                                                                         |
| Type of a value             | `type(items[0].price)`                                          | `"number"`                                                                       |
| String comparison           | `'a' < 'b'`                                                     | `null`                                                                           |

**API.** `POST /api/json/query` with `input` (the JSON text, at most 500,000 characters), `expression` (at most 2,048 characters) and `indent` (`2`, `4`, `tab` or `none`). The response has `result`, `type`, `json` (the formatted text) and `warnings`. Invalid JSON, an invalid expression and an evaluation error are 422 errors whose message gives the position.
