Jonq
Query JSON with SQL-like syntax. A readable jq alternative that generates pure jq under the hood. Table, CSV, YAML output. Interactive REPL. Pipes from curl, streams NDJSON logs.
Install / Use
/learn @duriantaco/JonqREADME
jonq — query JSON with SQL-like syntax from the terminal
A readable alternative to jq for JSON extraction, filtering, and exploration
</div>What is jonq?
jonq is a command-line JSON query tool that lets you select, filter, group, and reshape JSON data using SQL-like syntax instead of raw jq. It generates pure jq under the hood, so you get jq's speed with a syntax you can actually remember.
# Instead of: jq '.[] | select(.age > 30) | {name, age}'
jonq data.json "select name, age if age > 30" -t
name | age
--------|----
Alice | 35
Charlie | 42
jonq is not a database. It is a readable jq frontend for exploring, extracting, and reshaping JSON. If you need joins, window functions, or large-scale analytics, shape the JSON with jonq first and then hand it to DuckDB, Polars, or Pandas.
Use jonq when you need to
- Query JSON from APIs, config files, or log streams in the terminal
- Explore unfamiliar JSON with the built-in path explorer
- Write readable jq one-liners in shell scripts and CI pipelines
- Filter, aggregate, or reshape nested JSON without memorizing jq syntax
- Stream and filter NDJSON log output in real time
Use something else when you need
- Tabular analytics — DuckDB, Polars, Pandas
- Joins across files — a database or dataframe engine
- Large-scale ETL — tools built for analytical pipelines
Rule of thumb: if the problem is still "I need to understand this JSON", jonq is a good fit. If the problem has become relational analytics, move to a database.
Features at a glance
| Category | What you can do | Example |
|-------------------|-----------------|---------|
| Selection | Pick fields | select name, age |
| Wildcard | All fields | select * |
| DISTINCT | Unique results | select distinct city |
| Filtering | and / or / not / between / contains / in / like | if age > 30 and city = 'NY' |
| IS NULL | Null checks | if email is not null |
| Aggregations | sum avg min max count | select avg(price) as avg_price |
| COUNT DISTINCT| Unique counts | select count(distinct city) as n |
| Grouping | group by + having | ... group by city having count > 2 |
| Ordering | sort <field> [asc\|desc] | sort age desc |
| LIMIT | Standalone limit | select * limit 10 |
| CASE/WHEN | Conditional expressions | case when age > 30 then 'senior' else 'junior' end |
| COALESCE | Null fallback | coalesce(nickname, name) as display |
| String concat | + or \|\| | first \|\| ' ' \|\| last as full_name |
| Nested arrays | from [].orders or inline paths | select products[].name ... |
| String funcs | upper lower length trim | select upper(name) as name_upper |
| Math funcs | round abs ceil floor | select round(price) as price_r |
| Type casting | int float str type | select int(price) as price |
| Date/time | todate fromdate date | select todate(ts) as date |
| Inline maths | Field expressions | age + 10 as age_plus_10 |
| Table output | Aligned terminal tables | --format table or -t |
| YAML output | YAML rendering | --format yaml |
| CSV / stream | --format csv, --stream | |
| Follow mode | Stream NDJSON line-by-line | tail -f log \| jonq --follow "..." |
| Worker reuse | Reuse jq workers for repeated filters | --watch, --stream, Python loops |
| Path explorer | Inspect nested JSON paths and types | jonq data.json (no query) |
| Interactive REPL | Tab completion + history | jonq -i data.json |
| Watch mode | Re-run on file change | jonq data.json "select *" --watch |
| URL fetch | Query remote JSON | jonq https://api.example.com/data "select id" |
| Multi-file glob | Query across files | jonq 'logs/*.json' "select *" |
| Auto stdin | Auto-detect piped input | curl ... \| jonq "select id" |
| Auto NDJSON | Auto-detect line-delimited JSON | No flag needed |
| Shell completions | Bash/Zsh/Fish completions | jonq --completions bash |
| Explain mode | Show query breakdown + jq filter | --explain |
| Timing | Execution timing | --time |
| Fuzzy suggest | Typo correction for fields | Suggests similar field names |
| Colorized output | Syntax-highlighted JSON in terminal | Auto when TTY |
Why Jonq?
Jonq vs raw jq
| Task | Raw jq filter | jonq one-liner |
|------|------------------|--------------------|
| Select specific fields | jq '.[]|{name:.name,age:.age}' | jonq data.json "select name, age" |
| Filter rows | jq '.[]|select(.age > 30)|{name,age}' | ... "select name, age if age > 30" |
| Sort + limit | jq 'sort_by(.age) | reverse | .[0:2]' | ... "select name, age sort age desc 2" |
| Standalone limit | jq '.[0:5]' | ... "select * limit 5" |
| Distinct values | jq '[.[].city] | unique' | ... "select distinct city" |
| IN filter | jq '.[] | select(.city=="NY" or .city=="LA")' | ... "select * if city in ('NY', 'LA')" |
| NOT filter | jq '.[] | select((.age > 30) | not)' | ... "select * if not age > 30" |
| LIKE filter | jq '.[] | select(.name | startswith("Al"))' | ... "select * if name like 'Al%'" |
| Uppercase | jq '.[] | {name: (.name | ascii_upcase)}' | ... "select upper(name) as name" |
| Count items | jq 'map(select(.age>25)) | length' | ... "select count(*) as over_25 if age > 25" |
| Count distinct | jq '[.[].city] | unique | length' | ... "select count(distinct city) as n" |
| Group & count | jq 'group_by(.city) | map({city:.[0].city,count:length})' | ... "select city, count(*) as count group by city" |
| Group & HAVING | jq 'group_by(.city) | map(select(length>2)) | ...' | ... "select city, count(*) group by city having count > 2" |
| Field expression | jq '.[] | {name, age_plus: (.age + 10)}' | ... "select name, age + 10 as age_plus" |
| CASE/WHEN | jq '.[] | if .age>30 then "senior" else "junior" end' | ... "select case when age > 30 then 'senior' else 'junior' end as level" |
| COALESCE | jq '.[] | {d: (.nick // .name)}' | ... "select coalesce(nickname, name) as display" |
| IS NULL | jq '.[] | select(.email != null)' | ... "select * if email is not null" |
| String concat | jq '.[] | {f: (.first + " " + .last)}' | ... "select first || ' ' || last as full" |
| Type cast | jq '.[] | {p: (.price | tonumber)}' | ... "select float(price) as p" |
| Date convert | jq '.[] | {d: (.ts | todate)}' | ... "select todate(ts) as d" |
Take-away: a single jonq string replaces many pipes and brackets while still producing pure jq under the hood.
Where jonq fits
- Use jonq when the source of truth is still raw JSON and you need to inspect fields, paths, filters, or nested values quickly.
- Use raw jq when you already know the exact jq filter you want and do not need the friendlier syntax.
- Use DuckDB / Polars / Pandas after the JSON has become a tabular analytics problem.
TL;DR: jonq is the "understand and shape this JSON" step, not the database step.
Installation
Supported Platforms: Linux, macOS, and Windows with WSL.
Prerequisites
- Python 3.9+
jqcommand line tool installed (https://stedolan.github.io/jq/download/)
Setup
From PyPI
pip install jonq
From source
git clone https://github.com/duriantaco/jonq.git
cd jonq && pip install -e .
Quick Start
# Create a simple JSON file
echo '[{"name":"Alice","age":30,"city":"New York"},{"name":"Bob","age":25,"city":"LA"}]' > data.json
# Select fields
jonq data.json "select name, age if age > 25"
# Output: [{"name":"Alice","age":30}]
# Table output
jonq data.json "select name, age, city" -t
# Pipe from stdin (no '-' needed)
curl -s https://api.example.com/data | jonq "select id, name" -t
# Conditional expressions
jonq data.json "select name, case when age > 28 then 'senior' else 'junior' end as level" -t
# Null handling
jonq data.json "select coalesce(nickname, name) as display"
# Type casting
jonq data.json "select name, str(age) as age_str"
# String concatenation
jonq data.json "select name || ' (' || city || ')' as label"
# YAML output
jonq data.json "select name, age" -f yaml
# See what jq jonq generates
jonq data.json "select name, age if age > 25" --explain
Query Syntax
select [distinct] <fields> [from <path>] [if <condition>] [group by <fields> [having <condition>]] [sort <field> [asc|desc]] [limit N]
Where:
distinct- Optional, returns unique rows<fields>- Comma-separated: fields, aliases,CASE/WHEN,coalesce(), functions, aggregations, expressionsfrom <path>- Optional source path for nested dataif <condition>- Optional filter (supports=,!=,>,<,>=,<=,and,or,not,in,like,between,contains,is null,is not null)- `group
