---
title: "Learn jq by Example: Filter and Transform JSON | Prettify"
description: "A jq tutorial that starts with the idea the manual leaves out:
  every expression is a stream. Filter, map, pick, sort, and aggregate, with a
  cheat sheet."
url: https://prettify.tools/blog/learn-jq-by-example
---

# Learn jq by example

August 5, 2026 · 9 min read

Contents

- [The document](#the-document)
- [The one idea: everything is a stream](#the-one-idea-everything-is-a-stream)
- [Reaching into the document](#reaching-into-the-document)
- [\`.\[\]\` is the one that unlocks everything](#-is-the-one-that-unlocks-everything)
- [\`select()\` filters the stream](#select-filters-the-stream)
- [\`map()\` is \`.\[\]\` with the array put back](#map-is--with-the-array-put-back)
- [Picking fields](#picking-fields)
- [Sorting, grouping, aggregating](#sorting-grouping-aggregating)
- [Keys and entries](#keys-and-entries)
- [A real one](#a-real-one)
- [The part that makes this stick](#the-part-that-makes-this-stick)
- [Three things that will trip you up](#three-things-that-will-trip-you-up)
- [Cheat sheet](#cheat-sheet)

jq is the fastest way to pull one number out of a 40,000-line API response. It is also, for most people, the tool they give up on twice before it sticks.

The reason is not that jq is complicated. It is that the official manual is a _reference_: an alphabetical list of what every function does, written for someone who already has the mental model. It never tells you the one thing that makes the rest make sense.

This post tells you that thing first, then works through the queries you will actually use, on one realistic document.

Every query below was run through Prettify's jq engine and checked against jq 1.7.1 before publishing. The outputs are what the engines returned, not what I expected them to return.

## The document

```json
{
  "company": "Acme Corp",
  "founded": 2019,
  "headquarters": {
    "city": "San Francisco",
    "state": "CA",
    "coordinates": { "lat": 37.7749, "lng": -122.4194 }
  },
  "employees": [
    { "id": 1, "name": "Alice Johnson",  "role": "CEO",      "email": "alice@acme.com", "salary": 185000 },
    { "id": 2, "name": "Bob Smith",      "role": "CTO",      "email": "bob@acme.com",   "salary": 170000 },
    { "id": 3, "name": "Carol Williams", "role": "Designer", "email": "carol@acme.com", "salary": 125000 },
    { "id": 4, "name": "David Brown",    "role": "Engineer", "email": "david@acme.com", "salary": 145000 }
  ]
}
```

## The one idea: everything is a stream

Here is the thing the manual will not tell you.

**A jq expression does not return a value. It returns a stream of values: zero, one, or many.**

Not "an array sometimes". A stream. `.company` produces exactly one value. `.employees[]` produces four. `empty` produces none.

And the pipe does not mean "pass the result along". It means **"for each value on the left, run the right side"**.

Once that clicks, most of jq stops needing to be memorized. If you want the longer version of why, I wrote about [building a jq engine](https://prettify.tools/blog/writing-a-jq-engine-in-typescript), where this idea turns out to be the whole design.

## Reaching into the document

Start with `.`, which means "the whole thing". Then walk down with field names:

```
.company                          → "Acme Corp"
.headquarters.city                → "San Francisco"
.headquarters.coordinates.lat     → 37.7749
```

Missing fields give you `null` rather than an error:

```
.nope                             → null
```

That is deliberate, and it is why jq is pleasant against messy data. A missing key is not a crash.

## `.[]` is the one that unlocks everything

`.employees` gives you the array, as one value:

```
.employees | length               → 4
```

`.employees[]` is different. It **opens** the array and emits each element as its own value in the stream:

```
.employees[] | .name
→ "Alice Johnson"
  "Bob Smith"
  "Carol Williams"
  "David Brown"
```

Four separate outputs, not an array of four. That distinction is the whole game.

Read it as: for each employee, take `.name`.

You can also index directly:

```
.employees[0] | .name             → "Alice Johnson"
.employees[-1] | .name            → "David Brown"
```

Negative indices count from the end.

A colon takes a range instead of a single element, and gives you back an array:

```
.employees[0:2] | map(.name)      → ["Alice Johnson","Bob Smith"]
.employees[2:]  | map(.name)      → ["Carol Williams","David Brown"]
.employees[:1]  | map(.name)      → ["Alice Johnson"]
.employees[-2:] | map(.name)      → ["Carol Williams","David Brown"]
```

Either bound can be left out, and out-of-range bounds are clamped rather than treated as an error. Strings slice the same way, so `"hello world"[0:5]` is `"hello"`.

## `select()` filters the stream

`select(condition)` emits its input if the condition is true, and emits **nothing** if it is false. It is not a filter function in the JavaScript sense. It is a gate.

```
.employees[] | select(.salary > 150000) | .name
→ "Alice Johnson"
  "Bob Smith"
```

Four values went in, two came out. The other two hit the gate and produced no output at all.

Exact matches work the same way:

```
.employees[] | select(.role == "Engineer") | .name
→ "David Brown"
```

You can also filter on the result of an expression rather than a bare field. `test()` takes a regular expression:

```
.employees[] | select(.role | test("^C")) | .name
→ "Alice Johnson"
  "Bob Smith"
```

## `map()` is `.[]` with the array put back

This is where people get confused, so here it is directly.

- `.employees[] | .name` opens the array and gives you four separate values
- `.employees | map(.name)` keeps the array and gives you **one** value, an array of four

```
.employees | map(.name)
→ ["Alice Johnson","Bob Smith","Carol Williams","David Brown"]
```

Both are correct. Which you want depends on what comes next. If the next step needs an array, use `map`. If the next step should run once per item, use `.[]`.

The other way to put a stream back into an array is to wrap the whole thing in brackets:

```
[.employees[] | .name]
→ ["Alice Johnson","Bob Smith","Carol Williams","David Brown"]
```

`map(f)` is exactly `[.[] | f]`. Same thing, shorter.

You can combine `map` and `select` to filter and keep an array:

```
.employees | map(select(.salary > 150000)) | length
→ 2
```

## Picking fields

Curly braces build a new object. The shorthand copies a field by name:

```
.employees | map({name, salary})
→ [{"name":"Alice Johnson","salary":185000},
   {"name":"Bob Smith","salary":170000},
   {"name":"Carol Williams","salary":125000},
   {"name":"David Brown","salary":145000}]
```

Give the keys new names by writing them out:

```
.employees | map({label: .name, pay: .salary})
→ [{"label":"Alice Johnson","pay":185000}, …]
```

This is how you reshape an API response into whatever your code actually wants.

## Sorting, grouping, aggregating

```
.employees | sort_by(.salary) | map(.name)
→ ["Carol Williams","David Brown","Bob Smith","Alice Johnson"]
```

`reverse` flips it, so the highest paid is:

```
.employees | sort_by(.salary) | reverse | .[0].name
→ "Alice Johnson"
```

Though `max_by` says it more clearly:

```
.employees | max_by(.salary) | .name      → "Alice Johnson"
.employees | min_by(.salary) | .name      → "Carol Williams"
```

Totals and averages:

```
.employees | map(.salary) | add           → 625000
.employees | map(.salary) | add / length  → 156250
```

`add` sums the numbers in an array. In `add / length`, both sides get the same input, the array of salaries, so you get the sum divided by the count. Operands share an input the same way the two sides of `,` do.

Unique values and grouping:

```
.employees | map(.role) | unique          → ["CEO","CTO","Designer","Engineer"]
.employees | group_by(.role) | length     → 4
```

Joining into a string:

```
.employees | map(.name) | join(", ")
→ "Alice Johnson, Bob Smith, Carol Williams, David Brown"
```

## Keys and entries

```
.headquarters | keys
→ ["city","coordinates","state"]
```

Note that `keys` sorts alphabetically. If you need the original document order, use `keys_unsorted`.

`to_entries` turns an object into an array of `{key, value}` pairs, which is how you iterate over an object whose keys you do not know in advance:

```
.headquarters | to_entries
→ [{"key":"city","value":"San Francisco"},
   {"key":"state","value":"CA"},
   {"key":"coordinates","value":{"lat":37.7749,"lng":-122.4194}}]
```

The order is the document's, not alphabetical: unlike `keys`, `to_entries` leaves the pairs where it found them.

Once the object is an array, `map` and `select` work on it again, and `from_entries` puts it back:

```
.headquarters | to_entries | map(select(.value | type == "string")) | from_entries
→ {"city":"San Francisco","state":"CA"}
```

## A real one

Extracting the unique email domains from a list of people, in one line:

```
.employees | map(.email | split("@") | .[1]) | unique
→ ["acme.com"]
```

Read it left to right: for each employee, take the email, split it on `@`, take the second piece, then dedupe the resulting array. Every step is one of the pieces above.

## The part that makes this stick

Reading examples is not how you learn jq. You learn it by writing a query, being wrong, and seeing why.

Prettify has a **Transform Wizard** for exactly that loop. Pick a field, choose an operator, add a sort, and it writes the jq into the query box where you can read and edit it. It samples your document to discover the available fields, works out whether you have a root array or an array nested in an object, and offers only the operators that make sense for each field's type.

The point is not that the wizard writes queries for you. It is that **you can watch jq being written**, on your own data, and check your mental model against something that is definitely right. That feedback loop is what the manual cannot give you.

Once the generated query stops surprising you, you do not need the wizard.

**One thing to know up front:** the jq panel in Prettify is a Pro feature. Everything in this post is standard jq, so it works identically in the command-line tool, which is free. Learn it wherever you like.

## Three things that will trip you up

**`.[]` inside a viewer looks like an array.** Command-line jq prints each value in a stream on its own line. A GUI has to render one result, so a stream of four values is usually shown as an array of four. The semantics did not change, the display did. If you are following along in a browser tool and `.employees[]` looks identical to `.employees`, that is why.

**Errors are not `null`.** A missing key gives you `null`. A type error, such as indexing a number with a string, throws:

```
.founded.x        → error: cannot index number with string "x"
```

Append `?` and the expression swallows its own error and produces nothing:

```
.founded.x?       → (no output)
```

On the command line that prints nothing at all. In a viewer, a stream of zero values usually renders as `null`, for the same reason a stream of four renders as an array.

That is the difference between "this field is absent" and "this query does not make sense here", and jq keeps them separate on purpose.

**Strings come back quoted.** The output of jq is JSON, so on the command line `jq '.company'` prints `"Acme Corp"` with the quotes still attached. Pass `-r` for the raw string:

```
$ jq -r '.company' company.json
Acme Corp
```

That matters the moment you pipe jq into another command.

## Cheat sheet

| Goal                    | Query                                                 |
| ----------------------- | ----------------------------------------------------- |
| Whole document          | `.`                                                   |
| A field                 | `.company`                                            |
| Nested field            | `.headquarters.city`                                  |
| Array as one value      | `.employees`                                          |
| Each element separately | `.employees[]`                                        |
| First / last element    | `.employees[0]` / `.employees[-1]`                    |
| A range of elements     | `.employees[0:2]`, `.employees[2:]`, `.employees[:2]` |
| Count                   | `.employees \| length`                                |
| One field from each     | `.employees \| map(.name)`                            |
| Filter                  | `.employees[] \| select(.salary > 150000)`            |
| Filter, keep array      | `.employees \| map(select(.salary > 150000))`         |
| Regex filter            | `select(.role \| test("^C"))`                         |
| Pick fields             | `.employees \| map({name, salary})`                   |
| Rename fields           | `.employees \| map({label: .name})`                   |
| Sort                    | `.employees \| sort_by(.salary)`                      |
| Highest / lowest        | `max_by(.salary)` / `min_by(.salary)`                 |
| Sum                     | `.employees \| map(.salary) \| add`                   |
| Deduplicate             | `.employees \| map(.role) \| unique`                  |
| Join to a string        | `map(.name) \| join(", ")`                            |
| Object keys             | `.headquarters \| keys`                               |
| Key/value pairs         | `.headquarters \| to_entries`                         |
| Collect a stream        | `[.employees[] \| .name]`                             |
| Ignore errors           | `.founded.x?`                                         |
| Raw string output (CLI) | `jq -r '.company'`                                    |

---

_Try these against your own JSON in [Prettify](https://prettify.tools/), or start from the [JSON viewer](https://prettify.tools/json-viewer). Parsing runs in your browser, so nothing you paste leaves your device._

## Structured data

```json
{
  "@context": "https://schema.org",
  "@type": "SoftwareApplication",
  "name": "Prettify",
  "url": "https://prettify.tools",
  "description": "Multi-format data viewer, editor, and query engine for JSON, YAML, CSV, XML, Mermaid, Markdown, MDX, HTML, and SVG",
  "applicationCategory": "DeveloperApplication",
  "operatingSystem": "Web, macOS, Windows, Linux",
  "offers": [
    {
      "@type": "Offer",
      "price": "0",
      "priceCurrency": "USD",
      "description": "Free tier with core features"
    },
    {
      "@type": "Offer",
      "price": "19.99",
      "priceCurrency": "USD",
      "description": "Pro: lifetime access to all features"
    }
  ],
  "publisher": {
    "@id": "https://prettify.tools/#organization"
  }
}
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "@id": "https://prettify.tools/#organization",
  "name": "Prettify",
  "legalName": "Off Leash Labs",
  "url": "https://prettify.tools/",
  "logo": {
    "@type": "ImageObject",
    "url": "https://prettify.tools/icon-512.png",
    "width": 512,
    "height": 512
  },
  "contactPoint": {
    "@type": "ContactPoint",
    "contactType": "customer support",
    "email": "offleashlabs@gmail.com",
    "availableLanguage": "English"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "@id": "https://prettify.tools/blog/learn-jq-by-example",
  "url": "https://prettify.tools/blog/learn-jq-by-example",
  "headline": "Learn jq by example",
  "description": "A jq tutorial that starts with the idea the manual leaves out: every expression is a stream. Filter, map, pick, sort, and aggregate, with a cheat sheet.",
  "datePublished": "2026-08-05",
  "dateModified": "2026-08-05",
  "author": {
    "@type": "Organization",
    "name": "Prettify",
    "url": "https://prettify.tools"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Prettify",
    "url": "https://prettify.tools"
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://prettify.tools/blog/learn-jq-by-example"
  }
}
```
