CLI tool
davesnx/query-json avatar
davesnx/query-json

query-json: a jq-style JSON query language written in OCaml

Faster, simpler and more portable implementation of jq-inspired language in OCaml

640 stars11 forksOCamlBSD-3-Clause

At a glance

What is it?
query-json is a command line JSON query tool written in OCaml that started as a jq-compatible implementation and then diverged, with stricter null handling, readable function names, an interactive REPL and error messages that suggest a fix. It suits a team that wants jq ergonomics without jq's sharp edges, and it is a young project whose speed claim is its own.
Who is it for?
Adopt query-json if you script JSON often, you are willing to move away from jq's exact semantics, and you want the same query language in a shell, in the browser playground and in a Node process. Do not adopt it if your pipeline depends on byte-identical jq output, if you rely on jq behaviours it has not reimplemented, or if you need a guarantee on the speed comparison, because the figures come from the project's own benchmarks.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly OCaml, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A jq clone that decided to disagree

query-json is a fast, friendly and portable JSON query language for the command line, written in OCaml. The description in the repository is more precise: a faster, simpler and more portable implementation of a jq-inspired language.

The history matters for how you use it. It started as a jq-compatible implementation and evolved into something the author describes as better, with faster execution, better error messages and a modernised language design. The author is direct about the reason: most of jq's design is arcane and forced the project to be backwards compatible. They looked at it with fresh eyes and made a set of deliberate changes.

Those changes are enumerated in the README, and they are the substance of the project. All functions use snake_case. Null access is stricter, so the tool errors on a null instead of silently propagating it, and you use .foo? for optional access. Optional access works everywhere, including first?, last?, keys? and even first?(expr). Names are readable, with to_uppercase instead of ascii_upcase and starts_with instead of startswith. Defaults are saner, so group_by returns an object rather than nested arrays, and keys and unique preserve insertion order. There are extra functions such as filter, flat_map, find, pluck, compact and partition. And user-defined functions use the fn keyword rather than def.

The compatibility policy is the important practical detail. The old jq names still work but show deprecation warnings, and there is a full jq Compatibility Guide in docs/JQ_COMPATIBILITY.md with the details of each difference, including the behavioural differences, the additional features and the null handling rules.

The licence picture has a wrinkle. The README says MIT, while the project record for the repository says BSD-3-Clause. Both are permissive, but they are not the same licence, and the LICENSE file in the repository root is the one to read. The primary language is OCaml, the default branch is main, and the homepage is query-json.pages.dev.

Strict nulls and optional access change what a script means

The single most consequential difference from jq is the null handling, and it deserves more than a mention because it changes failure modes in existing scripts.

In jq, asking for a key that does not exist returns null, and the pipeline carries on. The README makes the contrast concrete by running the same typo through both tools. In jq the answer is simply null. In query-json the same query produces an error that names the key, shows the query position, lists the available keys and suggests the fix:

bash
$ query-json ".naem" <<< '{"name": "Alice", "age": 30}'
error[key_not_found]: key `naem` not found
  --> .naem
      ^^^^

  in: {"name": "Alice", "age": 30}
  available keys: name, age
  hint: use `.naem?` for optional access

The design decision is that a missing key is a mistake rather than a value. That is defensible, and it is better in exactly the case the README demonstrates, where a typo silently produced null and the bug lived on. It is worse in the case where a script probes for an optional field across many records and expects null for the ones that lack it, because the error stops the run instead.

The escape hatch is the question mark, and the project is thorough about it. Optional access applies to fields, and also to functions, with first?, last? and keys? and even the form first?(expr). So a script that wants optional semantics can ask for them explicitly rather than relying on a global default, which is the more honest arrangement of the two.

The second consequence is that a script written for jq cannot be ported by changing the binary. Every optional field access needs the question mark, and the results differ where the null used to flow. For a one-off command that is nothing. For a shell library in a build pipeline, it is a migration with a real cost, and the compatibility guide is the place to price it.

The remaining differences, the snake_case naming, the group_by shape, the insertion-order guarantees and the fn keyword, are smaller but they are the same kind of thing: a language that is close to jq rather than identical to it.

A REPL, a function index and a debug mode

The interactive side of query-json is the part that goes beyond being a faster jq.

The REPL has context-aware autocomplete, where typing a dot shows you the keys available in the current context and typing a pipe shows the functions you can call. That is a meaningful improvement over jq, which offers completions of a different kind, and it comes directly from the fact that the tool knows the shape of the data it is looking at.

The second feature is the built-in function reference, invoked with query-json --functions, which lists all functions by category. For a tool whose surface area has grown beyond jq's familiar core, being able to see the categories and the names is how you find out what exists.

The third is the error message design, discussed above, which is the more valuable of the two for a newcomer. The errors tell you why something failed and how to fix it, and the README treats that as a headline feature rather than a detail.

There is also a debug mode that is worth knowing about. The CLI options list includes --debug, described as printing lexer tokens and AST. So you can see exactly how the query was parsed, which is the fastest way to understand why a query is not doing what you think, and it is a debugging tool for the query language rather than for the data.

The rest of the options are the usual small set. There is -r and --raw-output to print strings without quotes, --no-color to disable colored output, --repl to start interactive mode, -v and --verbose which shows verbose output including deprecation warnings, and --version and --help. The verbose flag matters here in particular, because the deprecation warnings from old jq names are the mechanism by which the project tells you that your script is on the old path.

The command shape is a query followed by an optional file, and it reads from standard input when the file is omitted. That is what makes the piping examples in the README work.

Three ways to install it

The README gives three installation routes, and it is unusually honest about the first one.

The install script is the shortest:

bash
curl -sfL https://query-json.pages.dev/install.sh | bash

Before that, the README tells you to check the content of scripts/install.sh before running anything locally, and it links the well known argument that friends do not let friends pipe curl into bash. That warning belongs in a README rather than only in a blog post, and its presence is a reasonable signal about how the maintainer treats the security of the install path.

The npm route is the one most JavaScript-adjacent teams will use:

bash
npm install --global @davesnx/query-json

And the third option is to download a pre-built binary for macOS, Linux or Windows from the releases page, with no installer and no package manager involved.

The quick start examples show the shape of daily use. Querying a file is a query string and a path. Querying from standard input pipes a command into the tool, for example piping the output of curl for a public API into a query that builds an object with selected fields. Querying inline JSON puts the document in quotes as the second argument. The common operations section then shows the ordinary vocabulary: keys to list the keys of an object, a map with a select to filter array elements by a field, a map with an object literal to reshape records, group_by piped into map_values and length to count by category, and a chain of to_lowercase, split and first for string handling.

For development, the requirements are opam, and the README gives a three-command flow: clone the repository, then make init to create the opam switch and install dependencies, make dev-core to build query-json, and make test to run the tests. The project also documents how to run the playground locally, with make dev in one terminal to build and watch and make web-dev in another to start the web server.

The same language in a terminal, a browser and Node

The portability claim is specific rather than aspirational. The tool is written in OCaml and compiles to both native code and JavaScript, and the README lists the three targets as a CLI tool on macOS, Linux and Windows, the browser through the online playground, and a Node.js library installed with npm install @davesnx/query-json.

That is an unusual combination for a CLI tool, and it is a direct consequence of the implementation language. An OCaml compiler that emits JavaScript gives you a query engine that can run in a browser without a server, which is why the playground exists and why the same syntax you use in a script is what you use in a web page. The interactive playground is linked from the README, along with a separate function reference page that holds the complete list of built-in functions.

The website itself is a separate concern from the tool, and the repository layout shows it. There is a website/ directory, a js/ directory, a benchmarks/ directory with a README of detailed benchmarks, docs/, scripts/, cli/, source/, and a .opam file per component: query-json.opam, query-json-js.opam and query-json-playground.opam, with query-json.opam.template alongside them.

The web stack is in package.json, and the package there is named query-json-website with a version of 0.0.0, so it is a private site package rather than the published library. It requires Node 22 or newer and uses React and React DOM for the interface, the Monaco editor for the in-browser editing surface, lucide-react for icons, Tailwind CSS 4 for styling, and Vite with a monaco editor plugin for the build, plus wrangler for deployment and esbuild and rollup plugins in the build chain. There is also a .node-version file, which pins the Node version the project expects.

The Makefile confirms the split. The dune commands build the OCaml side, with build, build-prod, dev, dev-core and a repl target that runs the CLI with a package.json, while the Vite commands build and serve the website, with web-dev, web-build, web-preview and web-serve. The test targets run the unit tests through dune, with a test-watch variant and a test-promote target that updates snapshots and promotes them.

Where query-json is the wrong tool

The first limit is jq compatibility, which is partial by design. The language diverges on null handling, on function names, on group_by's return shape, on ordering guarantees, and on the keyword for user-defined functions. Old jq names work with deprecation warnings, but there is no guarantee that every jq behaviour has been reimplemented, and the compatibility guide is a long document with sections for behavioural differences and additional features rather than a short list. If your automation depends on exact jq semantics for edge cases, you need to read that guide rather than assume the tool is a drop-in.

The second is that the speed claim is the project's own. The README states that the native binary compiled with OCaml is consistently 2 to 4 times faster than jq across file sizes and operations, with a table giving 2.4 to 3 times faster below 10KB, 2 to 4.5 times faster at 100 to 500KB, and 1.6 to 3.3 times faster above 500KB, and it links to a benchmarks directory for detail. Those are the maintainer's numbers on the maintainer's benchmarks. The shape of the table is informative, since the advantage narrows at the largest sizes, but nobody should put these figures in a business case without running their own workloads.

The third is the extension story. The project is written in OCaml with a dune build and opam packages, and while a Node consumer can use the published npm package, a contributor extending the tool itself is working in an OCaml codebase with its own opam workflow. If your team is a JavaScript shop, the value is in consuming the compiled artefact, not in changing the parser.

The fourth is the young release line. The tags are 0.5.48 from 2025-11-14, 0.5.52 from 2025-11-16 and 0.6.1 from 2025-11-24, so the version moved quickly within ten days and has not been cut since, although the last push was on 2026-10-22. A project under 1.0 with a moving language design is a moving target for anyone pinning behaviour.

The fifth is the licence ambiguity I mentioned, MIT in the README and BSD-3-Clause in the project record, and a LICENSE file that settles it one way or the other. For a tool you might shell out to from a build script this is a minor concern, but for anything you redistribute it is not.

An OCaml project with a JavaScript front end

The repository tells a coherent story about how this is built. There is a dune-project file, a.ocamlformat file for formatting,.githooks/ and a setup-githooks target that points git's core.hooksPath at them, a CHANGES.md, and a Makefile whose help target is a generated list of commands.

The build and test targets are all dune invocations run through opam exec, which is the standard way to run dune inside an opam switch. The Makefile names the opam file as query-json.opam and the dune binary as opam exec -- dune, and points at the cli/ directory, with a project name of query-json. The project name variable is query-json and the dune wrapper is used for every build, test, format and clean step.

The technical topics are json, menhir, ocaml, reason-native and sedlex. Those last two tell you a lot about the implementation. menhir is the parser generator for the query language, so the grammar is a menhir file rather than hand-written recursive descent, and sedlex is the lexer, which is why the --debug option can print lexer tokens and the AST: both exist as artefacts you can inspect.

The reason-native tag is the most interesting one. It suggests the code is written in ReasonML, the syntax variant of OCaml, and that the project maintains a Reason-flavoured distribution alongside the OCaml one. That is consistent with the author, davesnx, being active in the Reason community, and with the Discord link in the support section pointing at a reasonml server.

The benchmarks directory is a first-class part of the repository rather than a script kept elsewhere, which tells you the author expects the speed comparison to be checked. The CHANGES.md file and the opam packages per component, one for the tool, one for the JavaScript build and one for the playground, show a project that distributes its three faces separately.

The open question for an adopter is what the 0.x version number is protecting you from. The project has clearly chosen to change behaviour deliberately, on nulls and on naming, and to warn rather than break on the old names. What that means in practice is that you should read the compatibility guide before migrating, and that you should expect the language to keep moving for now.

Editorial conclusion

Adopt query-json if you script JSON often, you are willing to move away from jq's exact semantics, and you want the same query language in a shell, in the browser playground and in a Node process. Do not adopt it if your pipeline depends on byte-identical jq output, if you rely on jq behaviours it has not reimplemented, or if you need a guarantee on the speed comparison, because the figures come from the project's own benchmarks. Verify first that the functions your scripts use exist in the compatibility guide, that the old jq names you rely on only warn rather than break, and that the licence question is settled, since the README says MIT while the project record says BSD-3-Clause, the newest release is 0.6.1 from 2025-11-24, and the last push was on 2026-10-22.

Frequently asked questions

How do I install query-json?

The README gives three options: the install script run with curl -sfL https://query-json.pages.dev/install.sh | bash, npm install --global @davesnx/query-json, or a pre-built binary for macOS, Linux and Windows from the releases page. The README asks you to read scripts/install.sh before running it locally.

Is query-json compatible with jq?

It started as a jq-compatible implementation but diverged deliberately: stricter null handling, snake_case function names, group_by returning an object, insertion-order preservation, extra functions, and fn instead of def for user-defined functions. The old jq names still work but show deprecation warnings, and the details are in the jq Compatibility Guide in docs/JQ_COMPATIBILITY.md.

What does query-json do differently when a key is missing?

It raises an error instead of returning null. The error names the key, points at the query position, lists the available keys and suggests using the optional form .foo? for optional access.

Can I use query-json in a browser or from Node.js?

Yes. It is written in OCaml and compiles to both native code and JavaScript, so it runs as a CLI on macOS, Linux and Windows, in the browser through the online playground, and as a Node.js library installed with npm install @davesnx/query-json.

Official sources

  1. davesnx/query-json on GitHub
  2. License: BSD-3-Clause
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/davesnx-query-json.svg)](https://hysenlabs.com/projects/davesnx-query-json)

Was this useful?