jql
The jql jackal logo

jql

A fast, lightweight command-line tool to query JSON.
Pronounce it jackal.

Get started The grammar GitHub
⚡

Fast by construction

Queries run against a simd-json tape, so only the part of the document you select is ever built.

ðŸŠķ

One small binary

No runtime, no configuration, no plugins. Install it and pipe JSON through it.

ðŸŽŊ

A grammar, not a language

Twelve tokens that compose. No expressions, no arithmetic, nothing to learn twice.

ðŸ’Ą

Errors that say why

A failed selection names the token and shows the value it was applied to.

Start

Getting started

A query is a sequence of tokens read left to right. Each token narrows what came before it, so a query describes a path through the document.

Keys are always double-quoted, because any string is a valid JSON key — including .valid, the empty string, and a lone quote. That means a query needs single quotes in a shell, so the double quotes survive.

echo '{ "name": "jql" }' | jql '"name"'

Read a file by passing it after the query, or pipe into it:

jql '"name"' package.json
cat package.json | jql '"name"'
Tip

Wrap the whole query in single quotes. Without them your shell eats the double quotes around each key, and the query will not parse.


Reference

The grammar

Every token, with an example you can paste into a shell. Each output below was produced by running jql 9.0.3 against the input beside it.

Object keys

The key selector is the one you will use most. Write selectors next to each other to walk down the document.

Select a key

A key selector is always double-quoted, so the query must be wrapped in single quotes in a shell.

$jql '"name"'
Input
{ "name": "jql", "stars": 1337 }
Output
"jql"

Walk down nested keys

Selectors sit next to each other with no separator. Each one descends one level.

$jql '"repo""owner""login"'
Input
{ "repo": { "owner": { "login": "yamafaktory" }, "private": false } }
Output
"yamafaktory"

Keys that need escaping

A key carrying a quote or a backslash is written the way JSON writes it. The full escape set is supported: \" \\ \/ \b \f \n \r \t and \uXXXX.

$jql '"a\"b"'
Input
{ "a\"b": 1, "plain": 2 }
Output
1

An empty key

The empty string is a valid JSON key, so it is a valid selector.

$jql '""'
Input
{ "": "empty keys are valid JSON" }
Output
"empty keys are valid JSON"

A key that does not exist

The error names the key and shows the object it was looked for in.

$jql '"missing"'
Input
{ "name": "jql" }
Error
Key "missing" doesn't exist in parent {"name":"jql"}

Arrays

Arrays are addressed by index or by range. Both accept several values, in any order you like.

Select one index

Indexes are zero-based.

$jql '[1]'
Input
["a", "b", "c", "d"]
Output
"b"

Select several indexes

Indexes are returned in the order you ask for them, not in the order they appear.

$jql '[2,0]'
Input
["a", "b", "c", "d"]
Output
[
  "c",
  "a"
]

A range

Ranges are inclusive at both ends.

$jql '[1:2]'
Input
["a", "b", "c", "d"]
Output
[
  "b",
  "c"
]

A reversed range

Writing the bounds backwards reverses the result. There is no separate reverse operator.

$jql '[2:0]'
Input
["a", "b", "c", "d"]
Output
[
  "c",
  "b",
  "a"
]

An open range

Leave a bound out and it runs to the end. [:1] does the same from the start.

$jql '[2:]'
Input
["a", "b", "c", "d"]
Output
[
  "c",
  "d"
]

The whole array

Both bounds omitted selects everything, which is useful as the last step of a pipe.

$jql '[:]'
Input
["a", "b", "c"]
Output
[
  "a",
  "b",
  "c"
]

An index out of bounds

The error reports the index and the array it was applied to.

$jql '[9]'
Input
["a", "b"]
Error
Index 9 in parent ["a","b"] is out of bounds

Objects

Objects can be addressed by name, and also by position — useful when you know the shape but not the keys.

Several keys at once

A multi key selector keeps the order you wrote, so it doubles as a way to reorder an object.

$jql '{"stars","name"}'
Input
{ "name": "jql", "stars": 1337, "forks": 42 }
Output
{
  "stars": 1337,
  "name": "jql"
}

Select keys by position

An object index selector picks entries by position rather than by name.

$jql '{1,0}'
Input
{ "name": "jql", "stars": 1337, "forks": 42 }
Output
{
  "stars": 1337,
  "name": "jql"
}

A range over positions

Object ranges behave like array ranges, including the reversed and open forms.

$jql '{0:1}'
Input
{ "name": "jql", "stars": 1337, "forks": 42 }
Output
{
  "name": "jql",
  "stars": 1337
}

A reversed object range

Same rule as arrays: bounds written backwards give a reversed result.

$jql '{2:1}'
Input
{ "name": "jql", "stars": 1337, "forks": 42 }
Output
{
  "forks": 42,
  "stars": 1337
}

Operators

Four operators reshape what a selector returned rather than descending into it.

Note

The keys operator returns keys sorted alphabetically, not in document order, and the truncate operator may appear only once, as the final token of a query.

Flatten nested arrays

The flatten operator collapses every level of nesting into a single array.

$jql '..'
Input
[[[["a"]], "b"], [["c"]]]
Output
[
  "a",
  "b",
  "c"
]

Flatten an object

On an object, flatten joins the path with dots and keeps the leaf value.

$jql '..'
Input
{ "a": { "b": { "c": 1 } }, "d": 2 }
Output
{
  "a.b.c": 1,
  "d": 2
}

List the keys of an object

The keys operator answers what is in here, which is the usual first move on an unfamiliar document. Keys come back sorted alphabetically, not in document order.

$jql '@'
Input
{ "zebra": 1, "apple": 2, "mango": 3 }
Output
[
  "apple",
  "mango",
  "zebra"
]

List the indexes of an array

On an array the same operator returns indexes rather than keys.

$jql '@'
Input
["a", "b", "c"]
Output
[
  0,
  1,
  2
]

Truncate to a primitive

Truncate maps the output to a bare primitive, turning a big array into [] and an object into {}. Useful to check a shape without printing it.

$jql '"repos"!'
Input
{ "repos": [
    { "name": "jql", "lang": "Rust", "stars": 1337, "archived": false },
    { "name": "hypergraphz", "lang": "Zig", "stars": 42, "archived": false },
    { "name": "old-thing", "lang": "Rust", "stars": 3, "archived": true }
  ] }
Output
[]

Truncate must come last

Only one truncate operator is allowed and it has to be the final token.

$jql '"repos"!"name"'
Input
{ "repos": [
    { "name": "jql", "lang": "Rust", "stars": 1337, "archived": false },
    { "name": "hypergraphz", "lang": "Zig", "stars": 42, "archived": false },
    { "name": "old-thing", "lang": "Rust", "stars": 3, "archived": true }
  ] }
Error
Truncate operator found as non last element or multiple times in Key Selector "repos", Truncate Operator, Key Selector "name"

Pipes

A pipe applies the tokens that follow to every element of an array, then optionally collapses the result back to a single value.

Apply a selector to every element

The pipe in operator applies everything that follows to each element of an array, collecting the results.

$jql '"repos"|>"name"'
Input
{ "repos": [
    { "name": "jql", "lang": "Rust", "stars": 1337, "archived": false },
    { "name": "hypergraphz", "lang": "Zig", "stars": 42, "archived": false },
    { "name": "old-thing", "lang": "Rust", "stars": 3, "archived": true }
  ] }
Output
[
  "jql",
  "hypergraphz",
  "old-thing"
]

Pick several fields from every element

Anything that works on one element works inside a pipe, including multi key selectors.

$jql '"repos"|>{"name","stars"}'
Input
{ "repos": [
    { "name": "jql", "lang": "Rust", "stars": 1337, "archived": false },
    { "name": "hypergraphz", "lang": "Zig", "stars": 42, "archived": false },
    { "name": "old-thing", "lang": "Rust", "stars": 3, "archived": true }
  ] }
Output
[
  {
    "name": "jql",
    "stars": 1337
  },
  {
    "name": "hypergraphz",
    "stars": 42
  },
  {
    "name": "old-thing",
    "stars": 3
  }
]

Stop piping and select from the result

The pipe out operator ends the iteration, so the tokens after it apply to the collected array as a whole.

$jql '"repos"|>"name"<|[0]'
Input
{ "repos": [
    { "name": "jql", "lang": "Rust", "stars": 1337, "archived": false },
    { "name": "hypergraphz", "lang": "Zig", "stars": 42, "archived": false },
    { "name": "old-thing", "lang": "Rust", "stars": 3, "archived": true }
  ] }
Output
"jql"

Reach into nested elements

After a pipe out you can range, index or slice the collected array like any other array.

$jql '"repos"|>"name"<|[2:1]'
Input
{ "repos": [
    { "name": "jql", "lang": "Rust", "stars": 1337, "archived": false },
    { "name": "hypergraphz", "lang": "Zig", "stars": 42, "archived": false },
    { "name": "old-thing", "lang": "Rust", "stars": 3, "archived": true }
  ] }
Output
[
  "old-thing",
  "hypergraphz"
]

Lenses

A lens filters an array, keeping the elements that match. It is the only token that removes elements rather than selecting them.

Keep elements that have a key

A lens with no value keeps every element where the selector resolves, regardless of what it holds.

$jql '|={"archived"}'
Input
[{ "name": "jql", "archived": false }, { "name": "note" }]
Output
[
  {
    "name": "jql",
    "archived": false
  }
]

Filter on a value

Give the lens a value and it keeps only the elements matching it. Values can be a boolean, null, a number or a string.

$jql '|={"lang"="Rust"}'
Input
[{ "name": "jql", "lang": "Rust" }, { "name": "hypergraphz", "lang": "Zig" }]
Output
[
  {
    "name": "jql",
    "lang": "Rust"
  }
]

Filter on a nested value

A lens takes a whole path, not just one key.

$jql '|={"owner""login"="yamafaktory"}'
Input
[{ "owner": { "login": "yamafaktory" } }, { "owner": { "login": "someone" } }]
Output
[
  {
    "owner": {
      "login": "yamafaktory"
    }
  }
]

Several conditions

Conditions separated by commas act as OR: an element is kept when any of them matches.

$jql '|={"lang"="Rust","archived"=true}'
Input
[{ "lang": "Rust", "archived": false }, { "lang": "Zig", "archived": true }, { "lang": "Rust", "archived": true }]
Output
[
  {
    "lang": "Rust",
    "archived": false
  },
  {
    "lang": "Zig",
    "archived": true
  },
  {
    "lang": "Rust",
    "archived": true
  }
]

Filter on a boolean

Booleans and null are written bare, without quotes.

$jql '|={"archived"=false}'
Input
[{ "name": "jql", "archived": false }, { "name": "old", "archived": true }]
Output
[
  {
    "name": "jql",
    "archived": false
  }
]

Groups

A comma splits a query into sub-queries, each run against the same input, collected into an array.

Build an array from sub-queries

A group separator runs each sub-query against the same input and collects the results into an array. Unlike a multi key selector it keeps no keys.

$jql '"name","stars"'
Input
{ "name": "jql", "stars": 1337, "forks": 42 }
Output
[
  "jql",
  1337
]

Combine unrelated paths

Each side of the comma is a full query, so groups reach into completely different parts of a document.

$jql '"repos"[0]"name","repos"[1]"lang"'
Input
{ "repos": [
    { "name": "jql", "lang": "Rust", "stars": 1337, "archived": false },
    { "name": "hypergraphz", "lang": "Zig", "stars": 42, "archived": false },
    { "name": "old-thing", "lang": "Rust", "stars": 3, "archived": true }
  ] }
Output
[
  "jql",
  "Zig"
]

Group whole pipelines

Sub-queries may contain pipes, ranges, anything. The group is the outermost structure.

$jql '"repos"|>"name"<|[0],"repos"|>"stars"'
Input
{ "repos": [
    { "name": "jql", "lang": "Rust", "stars": 1337, "archived": false },
    { "name": "hypergraphz", "lang": "Zig", "stars": 42, "archived": false },
    { "name": "old-thing", "lang": "Rust", "stars": 3, "archived": true }
  ] }
Output
[
  "jql",
  [
    1337,
    42,
    3
  ]
]

Putting it together

Real queries chain these pieces, and reading one from left to right tells you the whole pipeline. Every example below runs against this document:

{
  "meta": { "total": 3, "page": 1 },
  "repos": [
    { "name": "jql", "lang": "Rust", "stars": 1337, "archived": false,
      "owner": { "login": "yamafaktory", "id": 1 }, "topics": ["cli", "json"] },
    { "name": "hypergraphz", "lang": "Zig", "stars": 42, "archived": false,
      "owner": { "login": "yamafaktory", "id": 1 }, "topics": ["graph"] },
    { "name": "old-thing", "lang": "Rust", "stars": 3, "archived": true,
      "owner": { "login": "someone", "id": 2 }, "topics": [] }
  ]
}

Filter, then project

A lens narrows the array, then a pipe projects each survivor. This is the shape most real queries take.

$jql '"repos"|={"lang"="Rust"}|>"name"'
Output
[
  "jql",
  "old-thing"
]

Filter on a nested value

The lens path reaches into each element, so you can filter on something nested and still project from the top.

$jql '"repos"|={"owner""login"="yamafaktory"}|>"name"'
Output
[
  "jql",
  "hypergraphz"
]

Project several fields at once

A multi key selector inside a pipe reshapes every element, which is how you trim a wide API response down to what you need.

$jql '"repos"|={"archived"=false}|>{"name","stars"}'
Output
[
  {
    "name": "jql",
    "stars": 1337
  },
  {
    "name": "hypergraphz",
    "stars": 42
  }
]

Filter, project, take the first

Pipe out ends the iteration, so the index applies to the collected array rather than to each element.

$jql '"repos"|={"archived"=false}|>"owner""login"<|[0]'
Output
"yamafaktory"

Slice the piped result

Anything that works on an array works after a pipe out, including reversed ranges.

$jql '"repos"|>"name"<|[2:1]'
Output
[
  "old-thing",
  "hypergraphz"
]

Combine two unrelated pipelines

A group separator runs each side against the same document, so a summary field and a filtered list arrive together in one array.

$jql '"meta""total","repos"|={"lang"="Rust"}|>"name"'
Output
[
  3,
  [
    "jql",
    "old-thing"
  ]
]

Reach through a range into a pipe

Select a slice first and the pipe applies only to it, which is cheaper than piping the whole array and slicing after.

$jql '"repos"[0:1]|>"owner""login"'
Output
[
  "yamafaktory",
  "yamafaktory"
]

Keys of every element

The keys operator inside a pipe reports the shape of each element. Useful when elements are not uniform.

$jql '"repos"|>@<|[0]'
Output
[
  "archived",
  "lang",
  "name",
  "owner",
  "stars",
  "topics"
]

Flatten a nested selection

Piping collects an array of arrays; flatten then collapses it into one flat list. Flatten only unnests arrays — objects inside an array are left whole.

$jql '"repos"|>"topics"<|..'
Output
[
  "cli",
  "json",
  "graph"
]

Filter on a number

Lens values are not limited to strings. Here a number picks one element, and the projection keeps a nested object whole.

$jql '"repos"|={"stars"=1337}|>{"name","owner"}'
Output
[
  {
    "name": "jql",
    "owner": {
      "login": "yamafaktory",
      "id": 1
    }
  }
]

Mix array and object addressing

An array index picks the element, then an object range takes its first two entries by position. Selectors compose regardless of which kind of container they address.

$jql '"repos"[0]{0:1}'
Output
{
  "name": "jql",
  "lang": "Rust"
}

Using it

Many documents

Input may hold more than one JSON document. Each is evaluated on its own and produces its own result.

Several documents at once

Concatenated documents, with or without whitespace between them, are each evaluated and produce one result apiece.

$jql '"a"'
Input
{ "a": 1 }{ "a": 2 }{ "a": 3 }
Output
1
2
3

Documents separated by newlines

Newline-delimited JSON works the same way, which is what most log pipelines produce.

$jql '"name"'
Input
{ "name": "first" }
{ "name": "second" }
Output
"first"
"second"
Note

Anything trailing that is not itself a document is an error rather than being ignored, so a truncated stream fails loudly instead of returning partial results.

Flags

Eight flags, all short. The full list is in jql --help.

FlagWhat it does
-i, --inlinePrint the output on one line instead of pretty-printing it.
-q, --query <FILE>Read the query from a file rather than the command line.
-r, --raw-stringPrint a string result without its surrounding quotes.
-S, --sort-keysSort the keys of every object recursively, like jq -S.
-s, --streamProcess input line by line as it arrives, one document per line.
-v, --validateIgnore the query and report whether the input is valid JSON.
-h, --helpPrint help, including the whole grammar.
-V, --versionPrint the version.

Inline the output

Output is pretty-printed by default. Use -i or --inline for a single line, which is what you want when piping into another tool.

$jql --inline '"repos"[0]'
Input
{ "repos": [
    { "name": "jql", "lang": "Rust", "stars": 1337, "archived": false },
    { "name": "hypergraphz", "lang": "Zig", "stars": 42, "archived": false },
    { "name": "old-thing", "lang": "Rust", "stars": 3, "archived": true }
  ] }
Output
{"name":"jql","lang":"Rust","stars":1337,"archived":false}

Drop the quotes around a string

With -r or --raw-string a string result is printed bare, so it can be assigned to a shell variable.

$jql --raw-string '"repos"[0]"name"'
Input
{ "repos": [
    { "name": "jql", "lang": "Rust", "stars": 1337, "archived": false },
    { "name": "hypergraphz", "lang": "Zig", "stars": 42, "archived": false },
    { "name": "old-thing", "lang": "Rust", "stars": 3, "archived": true }
  ] }
Output
jql

Sort every object key

-S or --sort-keys sorts keys recursively, matching jq's -S. Handy for diffing two documents.

$jql --sort-keys --inline '"repo"'
Input
{ "repo": { "name": "jql", "archived": false, "lang": "Rust" } }
Output
{"archived":false,"lang":"Rust","name":"jql"}

Check that input is valid JSON

-v or --validate ignores the query and reports whether the input parses, setting the exit code accordingly.

$jql --validate ''
Input
{ "name": "jql" }
Output
Valid JSON file or content

Shell integration

jql eats JSON and writes JSON back, so it composes with everything else in a pipeline.

Save the output

jql '"repos"' input.json > output.json

Assign a value to a variable

Combine --raw-string with --inline so the result arrives without quotes or newlines:

name=$(jql -r -i '"name"' package.json)

Keep the query in a file

Long queries are easier to read, and to version, when they live on disk:

jql --query ./select-repos.jql input.json

Follow a stream

Without --stream the whole input is read before anything is written, which is right for a file or a finished pipe. With it, each line is processed as it arrives — for output still being produced:

docker logs --follow my-container | jql --stream '"message"'

It expects one document per line. A document spread over several lines needs the default mode.

Check exit codes

jql --validate input.json && echo "valid"

Installation

Packaged for most platforms, or grab a prebuilt binary.

PlatformCommand
Cargocargo install jql
Cargo Binstallcargo binstall jql
Alpine Linuxapk add jql
Arch Linuxyay -S jql
Fedoradnf install jql
FreeBSDpkg install jql
Homebrewbrew install jql
Nixnix-env -i jql
openSUSEzypper install jql

Prebuilt binaries for Linux, macOS and Windows are attached to every release.

Good to know

Errors say what went wrong

A failed selection reports the token that failed and the value it was applied to, rather than returning null and leaving you to guess.

It is not jq

There is no plan to align jql with jq or any similar tool. jql selects and reshapes; it has no expression language, no arithmetic and no user-defined functions, and that is deliberate.

Speed

Selection queries run against a simd-json tape, so only the part of the document a query actually selects is ever built. Measurements against jq live in PERFORMANCE.md.

Caveat

Two JSON parsers are in play, and they round the last bit of some scientific-notation literals differently, by up to two ULP. The tape returns the correctly rounded value. Plain decimals, integers and strings are unaffected.