# jet: a Clojure CLI that converts between JSON, EDN, YAML and Transit

> jet is a command line tool written in Clojure that transforms data between JSON, EDN, YAML and Transit, and unlike jq it can apply arbitrary Clojure functions to the data as it passes through. It suits Clojure developers who want to query and reshape structured data in a shell pipeline, and its startup time is the reason it is packaged as a GraalVM native binary rather than a jar.

**borkdude/jet** — CLI to transform between JSON, EDN, YAML and Transit using Clojure

- Repository: https://github.com/borkdude/jet
- Stars: 736 · Forks: 42
- Language: Clojure
- License: EPL-1.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/borkdude-jet

## A converter that lets you run Clojure over the data

jet is a CLI to transform between JSON, EDN, YAML and Transit, using Clojure. The description in the repository says it plainly, and the topic list names the formats and the toolchain: cli, clojure, converter, edn, graalvm, json, transit, yaml.

What separates it from a general purpose converter is that the transformation step is a Clojure function rather than a query in a purpose-built language. The README gives the example that shows the difference:

```bash
$ echo '{:a {:b {:c 1}}}' | jet --func '#(-> % :a :b :c)'
1
```

A converter that only changes format gives you the same data in a different syntax. jet lets you reach into it and compute something while you are there, and it can do so with the whole Clojure standard library, plus Specter.

The rationale section states the positioning plainly. This is a command line tool to transform between JSON, EDN and Transit using Clojure. It runs as a GraalVM binary with fast startup time, which makes it suited for shell scripting. It may seem familiar to users of `jq`.

That last sentence is a fair description of the audience, with one important difference in emphasis. jq is a filter language, and its power comes from a large set of built-in filters. jet's power comes from the fact that the filter is arbitrary Clojure code, so anything you can write in Clojure, including pulling in a library, is available as a transformation.

The formats are not equally first class in the implementation. The default for both `--from` and `--to` is edn, and EDN is the native representation, since it is what Clojure reads natively. JSON, YAML and Transit are the interchange formats around it.

## Installing as a native binary, a brew formula or a JVM library

The install options are several, and the order of preference in the README is a hint about who this is for.

The quickstart shows the one-liner installer:

```bash
$ bash < <(curl -s https://raw.githubusercontent.com/borkdude/jet/master/install)
$ echo '{:a 1}' | jet --to json
{"a":1}
```

By default this installs into `/usr/local/bin`, and to change the directory you provide the name:

```bash
$ bash <(curl -s https://raw.githubusercontent.com/borkdude/jet/master/install) /tmp
```

Linux and macOS binaries are also provided via brew, with `brew install borkdude/brew/jet` and `brew upgrade jet`. On Windows you can install using scoop and the scoop-clojure bucket, and there is an asdf version manager plugin. You may also download a binary directly from the GitHub releases.

The JVM route is documented in more detail than most projects give it, because the author is a Clojure developer and the tool is meant to be usable from inside a Clojure project. With Leiningen, you put a dependency in your `.lein/profiles`:

```clojure
{:user
 {:dependencies [[borkdude/jet "0.4.23"]]
  :aliases {"jet" ["run" "-m" "jet.main"]}}}
```

and then call it like `echo '["^ ","~:a",1]' | lein jet --from transit --to edn`.

With deps.edn the alias is similar:

```clojure
:jet {:deps {borkdude/jet {:mvn/version "0.4.23"}}
      :exec-fn jet.main/exec
      :main-opts ["-m" "jet.main"]}
```

and the README notes you can use either the `-M` or the `-X` style invocation, whichever you prefer, showing both:

```clojure
$ echo '[1 2 3]' | clj -M:jet --colors --func '#(-> % first inc)'
2

$ echo '[1 2 3]' | clj -X:jet :colors true :thread-last '"(map inc)"'
(2 3 4)
```

There is a third route, installing it as a clj tool:

```clojure
$ clojure -Ttools install-latest :lib io.github.borkdude/jet :as jet

$ echo '[1 2 3]' | clj -Tjet exec :colors true :func '"#(-> % first inc)"'
2
```

The repository layout explains how the project supports all of these at once. There is a `pom.xml` and a `project.clj` for the Maven and Leiningen builds, a `deps.edn` for tools.deps, an `install` script at the root, `src-java/` for the Java sources of the native binary, and `script/` for the build scripts. It is published to Clojars, and there is a cljdoc badge for the API documentation.

## The option list, and a query flag that is already deprecated

The usage section lists the supported options, and one of them tells you the project is moving.

```bash
  -i, --from            [ edn | transit | json | yaml ] defaults to edn.
  -o, --to              [ edn | transit | json | yaml ] defaults to edn.
  -t, --thread-last                                     implicit thread last
  -T, --thread-first                                    implicit thread first
  -f, --func                                            a single-arg Clojure function, or a path to a file that contains a function, that transforms input.
      --no-pretty                                       disable pretty printing
  -k, --keywordize      [ <key-fn> ]                    if present, keywordizes JSON/YAML keys. The default transformation function is keyword unless you provide your own.
      --colors          [ auto | true | false]          use colored output while pretty-printing. Defaults to auto.
      --edn-reader-opts                                 options passed to the EDN reader.
      --no-commas                                       remove commas from EDN
  -c, --collect                                         given separate values, collects them in a vector.
  -h, --help                                            print this help text.
  -v, --version                                         print the current version of jet.
  -q, --query                                           DEPRECATED, prefer -t, -T or -f. Given a jet-lang query, transforms input.
```

The query flag is the remnant of the jq-style approach, and it is marked deprecated with a pointer to the three that replaced it. That history explains the design: jet started with a query language of its own, and then grew the ability to pass real Clojure, at which point the query language was no longer the better interface.

The `--func` option takes either a Clojure function or a path to a file containing one, which is the practical distinction for scripts, since a long function in a shell pipeline is unpleasant to maintain:

```bash
$ cat /tmp/fn.clj
#(-> % :a :b :c)
$ echo '{:a {:b {:c 1}}}' | jet --func /tmp/fn.clj
1
```

The `--keywordize` option is worth understanding because it is the bridge from JSON to Clojure data. When it is present, JSON and YAML keys are keywordized, and the default transformation is `keyword` unless you provide your own function. So `{"a": 1}` becomes `{:a 1}`, which is then navigable with `:a` in a thread-last expression.

The other options are small but they add up. `--edn-reader-opts` passes options to the EDN reader, `--no-commas` removes commas from EDN output, `--no-pretty` disables pretty printing, `--colors` controls coloured output while pretty printing with a default of auto, and `--collect` gathers separate values into a vector.

## Streaming, tagged literals and raw output

Three features make jet usable in real pipelines rather than only on single small documents.

The first is streaming. Jet supports streaming over multiple values, without reading the entire input into memory:

```bash
$ echo '{"a": 1} {"a": 1}' | jet --from json --keywordize -t ':a' --to edn
1
1
```

For a log file with one object per line, that is the difference between a tool that works and a tool that runs out of memory. When you want the values gathered instead of streamed, `--collect` puts them in a vector:

```bash
$ echo '{"a": 1} {"a": 1}' | lein jet --from json --keywordize --collect --to edn
[{:a 1} {:a 1}]
```

The second is data readers. You can enable them by passing options to `--edn-reader-opts`, which is how tagged literals survive a round trip:

```bash
$ echo '#foo{:a 1}' | jet --edn-reader-opts '{:default tagged-literal}'
#foo {:a 1}
$ echo '#foo{:a 1}' | jet --edn-reader-opts "{:readers {'foo (fn [x] [:foo x])}}"
[:foo {:a 1}]
```

The first form keeps the tag as a tagged literal, and the second installs a reader function that turns the tag into a value. Since jet 0.0.14 the `--edn-reader-opts` option defaults to `{:default tagged-literal}`, so tagged literals are handled out of the box.

The third is raw output. Get raw output from a query rather than wrapped in quotes:

```shellsession
$ echo '{"a": "hello there"}' | jet --from json --keywordize -t ":a" --to edn
"hello there"

$ echo '{"a": "hello there"}' | jet --from json --keywordize -t ":a symbol" --to edn
hello there
```

or, as the README puts it, simply use `println` to get rid of the quotes.

Specter is available too. As of version `0.2.18` the specter library is available in `--func`, `--thread-first` and `--thread-last`, which is what makes the deep nested update possible:

```clojure
$ echo '{:a {:a 1}}' | ./jet -t '(s/transform [s/MAP-VALS s/MAP-VALS] inc)'
{:a {:a 2}}
```

And for the base64 case, the README notes you can use `base64/encode` and `base64/decode`.

## Against jq, and against a Python one-liner with the standard library

The comparison with jq is the one the project itself raises, and the difference in approach is worth stating in terms of what you have to learn.

jq is a filter language. Its power comes from a large set of built-in filters, and its composition rules are compact enough that a jq expression is close to unreadable to anyone who does not write jq every day. jet's transformation is a Clojure expression, so the filter is drawn from a language with a specification, a standard library and a large body of existing practice, and it can pull in any library you have on the classpath.

The cost is the mirror image. A Clojure expression is longer than a jq expression for the same simple job, and it assumes a reader who can parse Clojure syntax. A pipeline of a dozen jq filters is a pipeline of a dozen short strings, while the equivalent in jet is a dozen short strings too, but written in a different notation and with the possibility of using a named function from a file when the expression outgrows a shell argument.

The second alternative is not a command line tool at all. A Python script with the standard library can do the same conversions, and Python is on more machines than a Clojure runtime. The reason to prefer jet over that is startup time and dependencies: a Python interpreter with its json module is fast enough for a shell pipeline, but a script that has to import a YAML library or handle tagged literals is doing more work than jet's native binary does.

What jet is not is a general data tool. It has no file arguments, no multi-file processing and no output to a file, so it is a filter in a pipe, not a tool you point at a directory. The streaming support makes it work on a continuous stream, which covers the log file case that a script would have to implement with a loop.

The GraalVM packaging is the quiet decision underneath all of this. A JVM program on the command line pays startup cost on every invocation, and in a shell pipeline that cost is paid per element of the pipeline, not once. Compiling to a native binary removes it, which is what makes a Clojure-based tool viable in that position at all.

## Format conversion is the easy half, and the examples show it

The example set in the README is a quick catalogue of what the format conversion does, and it is worth reading as the baseline against which the function support is the addition.

```bash
$ echo '{"a": 1}' | jet --from json --to edn
{"a" 1}

$ echo '{"a": 1}' | jet -i json --keywordize -o edn
{:a 1}

$ echo '{"my key": 1}' | jet -i json -k '#(keyword (str/replace % " " "_"))' -o edn
{:my_key 1}

$ echo '{"anApple": 1}' | jet -i json -k '#(-> % csk/->kebab-case keyword)' -o edn
{:an-apple 1}

$ echo '{"a": 1}' | jet -i json -o yaml
a: 1

$ echo '{"a": 1}' | jet -i json -o transit
["^ ","a",1]
```

Two things to note. The keywordize examples show that the keyword function is arbitrary Clojure, and the second of them uses `csk/->kebab-case`, which is not a built-in, so the conversion function can pull in a namespace beyond the core. The transit output is a JSON array, which is the Transit JSON encoding with the `^ ` marker at the front, so Transit output is readable by tools that only understand JSON.

The traversal examples cover the three ways of expressing the same thing:

```bash
$ echo '{:a {:b {:c 1}}}' | jet --thread-last ':a :b :c'
1

$ echo '{:a {:b {:c 1}}}' | jet --func '#(-> % :a :b :c)'
1

$ echo '{:a {:b {:c [1 2]}}}' | jet -t ':a :b :c (map inc)'
(2 3)
```

The last one shows that the thread-last form is not limited to a path. It takes a sequence of forms and a trailing expression, so you can navigate and then transform, which is the idiom a Clojure developer already has in their fingers.

The project is published on Clojars, has a CircleCI badge and a cirrus config for CI, a `bb.edn` for babashka tasks, a `.clj-kondo/` directory for linting, a CHANGELOG.md, and a `doc/` directory. The licence is EPL-1.0, and the last push was on 2026-09-11 with releases v0.7.27, v0.6.26 and v0.5.25 from 2023.

## Conclusion

Adopt jet when your shell work already lives in Clojure and you want to read JSON or YAML and act on it with real Clojure functions, including Specter and tagged literals, rather than with a query language invented for the purpose. Do not adopt it if you need something to run on a machine with no JVM and no native binary for that platform, and check first that the version you install supports the option you need, since the project has renamed flags across releases and the query flag is already marked deprecated in favour of thread-last, thread-first and func.

## FAQ

### What does jet do?

It is a Clojure command line tool that transforms data between JSON, EDN, YAML and Transit, and can apply arbitrary Clojure functions to the data while it passes through. The default input and output format is EDN.

### How do I install jet?

The installer script `bash <(curl -s https://raw.githubusercontent.com/borkdude/jet/master/install)` puts it in /usr/local/bin by default. Linux and macOS binaries are also on brew as `brew install borkdude/brew/jet`, Windows users can use scoop with the scoop-clojure bucket, there is an asdf plugin, and binaries are downloadable from the GitHub releases.

### How do I use jet from a Clojure project?

With Leiningen you add the dependency and an alias to your .lein/profiles, with deps.edn you add a :jet alias using :exec-fn jet.main/exec, and you can also install it as a clj tool with `clojure -Ttools install-latest :lib io.github.borkdude/jet :as jet`. The README shows both the -M and -X invocation styles.

### Can jet handle large input without loading it all into memory?

Yes, it supports streaming over multiple values without reading the entire input into memory, and the --collect option gathers separate values into a vector instead when you want that. The README shows both forms on a two-object JSON stream.

### Is the --query option still supported?

It is deprecated, and the help text says to prefer -t, -T or -f. The query language was jet's own earlier approach and the three replacements correspond to thread-last, thread-first and a Clojure function given with --func, which can also be a path to a file containing the function.

### Is jet still being maintained?

The repository is not archived and the last push was on 2026-09-11. The listed releases are v0.7.27 from 2023-08-02, v0.6.26 from 2023-07-03 and v0.5.25 from 2023-04-26, and the licence is EPL-1.0 with the package published to Clojars.

## Sources

- [borkdude/jet on GitHub](https://github.com/borkdude/jet)
- [Issues](https://github.com/borkdude/jet/issues)
- [License: EPL-1.0](https://github.com/borkdude/jet/blob/master/LICENSE)
- [README](https://github.com/borkdude/jet/blob/master/README.md)
- [Releases](https://github.com/borkdude/jet/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/borkdude-jet
