Skip to main content

Readable jq for JSON extraction and exploration

Project description

jonq Logo

jonq

Readable jq for JSON extraction and exploration

PyPI version Python Versions CI tests Documentation Status License: MIT Skylos Grade


About

jq is unbeatable for JSON processing, but its syntax is easy to forget when you just need to inspect an API payload, grab a nested field, or reshape a response in the terminal. jonq is a JSON-native CLI that makes those jobs readable again while still generating pure jq under the hood.

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 a real analytical tool.


What jonq IS for

  • You want to inspect an API response, config file, or log payload quickly
  • You want a built-in path explorer before writing a query
  • You need a readable jq one-liner in a shell script or CI pipeline
  • You want to extract or reshape nested JSON without memorizing jq syntax

What jonq is NOT for

  • Tabular analytics — use DuckDB, Polars, Pandas, or a real database
  • Joins across files — use a database or dataframe engine
  • Large-scale ETL — use tools built for analytical pipelines
  • Business intelligence — use proper BI tools

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 '.[]&#124;{name:.name,age:.age}' jonq data.json "select name, age"
Filter rows jq '.[]&#124;select(.age > 30)&#124;{name,age}' ... "select name, age if age > 30"
Sort + limit jq 'sort_by(.age) &#124; reverse &#124; .[0:2]' ... "select name, age sort age desc 2"
Standalone limit jq '.[0:5]' ... "select * limit 5"
Distinct values jq '[.[].city] &#124; unique' ... "select distinct city"
IN filter jq '.[] &#124; select(.city=="NY" or .city=="LA")' ... "select * if city in ('NY', 'LA')"
NOT filter jq '.[] &#124; select((.age > 30) &#124; not)' ... "select * if not age > 30"
LIKE filter jq '.[] &#124; select(.name &#124; startswith("Al"))' ... "select * if name like 'Al%'"
Uppercase jq '.[] &#124; {name: (.name &#124; ascii_upcase)}' ... "select upper(name) as name"
Count items jq 'map(select(.age>25)) &#124; length' ... "select count(*) as over_25 if age > 25"
Count distinct jq '[.[].city] &#124; unique &#124; length' ... "select count(distinct city) as n"
Group & count jq 'group_by(.city) &#124; map({city:.[0].city,count:length})' ... "select city, count(*) as count group by city"
Group & HAVING jq 'group_by(.city) &#124; map(select(length>2)) &#124; ...' ... "select city, count(*) group by city having count > 2"
Field expression jq '.[] &#124; {name, age_plus: (.age + 10)}' ... "select name, age + 10 as age_plus"
CASE/WHEN jq '.[] &#124; if .age>30 then "senior" else "junior" end' ... "select case when age > 30 then 'senior' else 'junior' end as level"
COALESCE jq '.[] &#124; {d: (.nick // .name)}' ... "select coalesce(nickname, name) as display"
IS NULL jq '.[] &#124; select(.email != null)' ... "select * if email is not null"
String concat jq '.[] &#124; {f: (.first + " " + .last)}' ... "select first &#124;&#124; ' ' &#124;&#124; last as full"
Type cast jq '.[] &#124; {p: (.price &#124; tonumber)}' ... "select float(price) as p"
Date convert jq '.[] &#124; {d: (.ts &#124; 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

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, expressions
  • from <path> - Optional source path for nested data
  • if <condition> - Optional filter (supports =, !=, >, <, >=, <=, and, or, not, in, like, between, contains, is null, is not null)
  • group by <fields> - Optional grouping by one or more fields
  • having <condition> - Optional filter on grouped results
  • sort <field> [asc|desc] - Optional ordering
  • limit N - Optional result count limit

Examples

Given this JSON (simple.json):

[
  { "id": 1, "name": "Alice",   "age": 30, "city": "New York"    },
  { "id": 2, "name": "Bob",     "age": 25, "city": "Los Angeles" },
  { "id": 3, "name": "Charlie", "age": 35, "city": "Chicago"     }
]

Selection

jonq simple.json "select *"                    # all fields
jonq simple.json "select name, age"            # specific fields
jonq simple.json "select name as full_name"    # with alias

DISTINCT

jonq simple.json "select distinct city"
# [{"city":"Chicago"},{"city":"Los Angeles"},{"city":"New York"}]

Filtering

jonq simple.json "select name, age if age > 30"
jonq simple.json "select name if age > 25 and city = 'New York'"
jonq simple.json "select name if age > 30 or city = 'Los Angeles'"
jonq simple.json "select name if age between 25 and 30"

IN Operator

jonq simple.json "select * if city in ('New York', 'Chicago')"
# [{"id":1,"name":"Alice","age":30,"city":"New York"},{"id":3,"name":"Charlie","age":35,"city":"Chicago"}]

NOT Operator

jonq simple.json "select * if not age > 30"
# [{"id":1,"name":"Alice","age":30,"city":"New York"},{"id":2,"name":"Bob","age":25,"city":"Los Angeles"}]

LIKE Operator

jonq simple.json "select * if name like 'Al%'"     # starts with "Al"
jonq simple.json "select * if name like '%ice'"     # ends with "ice"
jonq simple.json "select * if name like '%li%'"     # contains "li"

Sorting and Limiting

jonq simple.json "select name, age sort age desc"
jonq simple.json "select name, age sort age desc 2"   # sort + inline limit
jonq simple.json "select * limit 2"                    # standalone limit

Aggregation

jonq simple.json "select sum(age) as total_age"
jonq simple.json "select avg(age) as average_age"
jonq simple.json "select count(*) as total"
jonq simple.json "select count(distinct city) as unique_cities"

GROUP BY and HAVING

jonq simple.json "select city, count(*) as cnt group by city"
jonq simple.json "select city, avg(age) as avg_age group by city"
jonq simple.json "select city, count(*) as cnt group by city having cnt > 0"

String Functions

jonq simple.json "select upper(name) as name_upper"
# [{"name_upper":"ALICE"},{"name_upper":"BOB"},{"name_upper":"CHARLIE"}]

jonq simple.json "select lower(city) as city_lower"
jonq simple.json "select length(name) as name_len"

Math Functions

jonq simple.json "select round(age) as rounded_age"
jonq simple.json "select abs(age) as abs_age"
jonq simple.json "select ceil(age) as ceil_age"
jonq simple.json "select floor(age) as floor_age"

Nested JSON

# nested field access
jonq nested.json "select name, profile.address.city"

# from: select from nested arrays
jonq complex.json "select name, type from products"

# boolean logic with nested fields
jonq nested.json "select name if profile.address.city = 'New York' or orders[0].price > 1000"

CASE/WHEN Expressions

jonq simple.json "select name, case when age > 30 then 'senior' when age > 25 then 'mid' else 'junior' end as level"
# [{"name":"Alice","level":"mid"},{"name":"Bob","level":"junior"},{"name":"Charlie","level":"senior"}]

COALESCE

jonq data.json "select coalesce(nickname, name) as display_name"
# Falls back to name when nickname is null

# Works with nested functions
jonq data.json "select coalesce(todate(timestamp), 'unknown') as date"

IS NULL / IS NOT NULL

jonq data.json "select name if email is not null"
jonq data.json "select name if nickname is null"

String Concatenation

# Using || (SQL standard)
jonq simple.json "select name || ' from ' || city as label"

# Using + (also works)
jonq simple.json "select name + ' from ' + city as label"

Type Casting

jonq data.json "select int(price) as price"        # string → integer
jonq data.json "select float(amount) as amount"     # string → float
jonq data.json "select str(code) as code"           # number → string
jonq data.json "select type(value) as t"            # get type name

Date/Time Functions

jonq data.json "select todate(timestamp) as date"   # epoch → ISO date
jonq data.json "select date(created_at) as d"       # alias for todate

Arithmetic Expressions

jonq simple.json "select name, age + 10 as age_plus_10"

Output Formats

Table Output

jonq simple.json "select name, age, city" -t
# name    | age | city
# --------|-----|-------------
#  Alice   | 30  | New York
#  Bob     | 25  | Los Angeles
#  Charlie | 35  | Chicago

CSV Output

jonq simple.json "select name, age" --format csv > output.csv

YAML Output

jonq simple.json "select name, age" --format yaml
# - name: Alice
#   age: 30
# - name: Bob
#   age: 25

Python API

from jonq import compile_query, query

data = [
    {"name": "Alice", "age": 30, "city": "New York"},
    {"name": "Bob", "age": 25, "city": "LA"},
]

compiled = compile_query("select name, city if age > 26")
result = query(data, compiled)
print(result)

Output:

[{"name": "Alice", "city": "New York"}]

If you want metadata such as the generated jq filter, use execute(...) instead of query(...).

Repeated identical filters reuse a live jq worker in long-lived Python processes, which reduces jq startup overhead in loops and services.

Streaming Mode

For processing large root-array JSON files more efficiently:

jonq large.json "select name, age" --stream

Chunk execution stays in memory and reuses the same jq worker for the generated filter instead of writing chunk temp files and starting a fresh jq subprocess for every chunk.

Path Explorer

Run jonq with just a file (no query) to inspect nested JSON paths before writing a query:

jonq data.json

Output:

data.json  (array, sampled 3 items)

Paths:
  id             int               1
  name           str               "Alice"
  age            int               30
  city           str               "New York"
  orders[]       array[object]
  orders[].id    int               1
  orders[].item  str               "Laptop"

Sample:
  { "id": 1, "name": "Alice", "age": 30, "city": "New York", "orders": [{ "id": 1, "item": "Laptop" }] }

Tip: jonq data.json "select name, orders[].item"

Interactive REPL

Launch an interactive session with tab completion and persistent history:

jonq -i data.json
jonq interactive mode — querying data.json
Type a query, or 'quit' to exit. Tab completes field names.
Example: select name, age if age > 30

jonq> select name, age
[{"name":"Alice","age":30},{"name":"Bob","age":25}]
jonq> select * if age > 28
[{"id":1,"name":"Alice","age":30,"city":"New York"}]
jonq> quit

Features:

  • Tab completion for field names and SQL keywords
  • Persistent history saved to ~/.jonq_history
  • Up/down arrow to recall previous queries

Watch Mode

Re-run a query automatically whenever the file changes:

jonq data.json "select name, age" --watch

Because watch mode reruns the same filter repeatedly inside one loop, jonq reuses a live jq worker to reduce refresh overhead.

Multiple Input Sources

URL Fetch

jonq https://api.example.com/users.json "select name, email"

Multi-File Glob

jonq 'logs/*.json' "select * if level = 'error'"

Stdin

# Auto-detected — no '-' needed
curl -s https://api.example.com/data | jonq "select id, name"

# Explicit stdin still works
cat data.json | jonq - "select name, age"

Follow Mode

Stream NDJSON from stdin line-by-line, applying the query to each object as it arrives:

tail -f app.log | jonq --follow "select level, message if level = 'error'" -t

Only matching lines are printed. Non-matching lines are silently skipped.

Auto-detect NDJSON

jonq auto-detects NDJSON (newline-delimited JSON) files. No flag needed:

jonq data.ndjson "select name, age if age > 25"

You can still force it with --ndjson if needed. --ndjson cannot be combined with --stream.

Fuzzy Field Suggestions

When you mistype a field name, jonq suggests similar fields:

$ jonq data.json "select nme, agge"
Field(s) 'nme, agge' not found. Available fields: age, city, id, name. Did you mean: 'nme' -> name; 'agge' -> age?

CLI Options

Option Description
--format, -f Output format: json (default), csv, table, yaml
-t, --table Shorthand for --format table
--stream, -s Process root-array JSON in memory-friendly chunks
--ndjson Force NDJSON mode (auto-detected by default)
--follow Stream NDJSON from stdin line-by-line
--limit, -n N Limit rows post-query
--out, -o PATH Write output to file
--jq Print generated jq filter and exit
--explain Show parsed query breakdown and generated jq filter
--time Print execution timing to stderr
--pretty, -p Pretty-print JSON output
--watch, -w Re-run query when file changes
--no-color Disable colorized output
--completions SHELL Print shell completion script (bash, zsh, fish)
--version Show the installed jonq version
-i <file> Interactive query mode (REPL) with tab completion
-h, --help Show help message

Shell Completions

Generate completion scripts for your shell:

# Bash
eval "$(jonq --completions bash)"

# Zsh
eval "$(jonq --completions zsh)"

# Fish
jonq --completions fish > ~/.config/fish/completions/jonq.fish

Colorized Output

When outputting to a terminal, jonq auto-pretty-prints and colorizes JSON. Pipe to a file or use --no-color to disable.

Troubleshooting

Common Errors

Command 'jq' not found - Make sure jq is installed and in your PATH. Install: https://stedolan.github.io/jq/download/

Invalid JSON in file - Check your JSON file for syntax errors. Use a JSON validator.

Syntax error in query - Verify your query follows the correct syntax. Check field names and quotes.

Runtime jq error - Errors like Cannot iterate over string or Cannot iterate over null are surfaced immediately. Adjust the field path or inspect the input with jonq data.json.

No results returned - Verify your condition isn't filtering out all records. Check field name casing.

Known Limitations

  • Performance: For very large JSON files (100MB+), processing may still be slow. --stream is more memory-friendly now, but jonq is still not an analytical engine.
  • Advanced jq Features: Some advanced jq features (recursive descent, custom filters) aren't exposed in the jonq syntax.
  • Custom Functions: User-defined functions aren't supported.
  • Joins: Cross-file joins are not supported — use a database for relational queries.
  • Window Functions: Not supported — use DuckDB or Polars for analytical queries.

Docs

Full documentation: https://jonq.readthedocs.io/en/latest/

See also: SYNTAX.md for the complete syntax reference.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Misc.

The jq tool itself is not included in this package - users need to install it separately.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jonq-0.3.0.tar.gz (629.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jonq-0.3.0-py3-none-any.whl (47.6 kB view details)

Uploaded Python 3

File details

Details for the file jonq-0.3.0.tar.gz.

File metadata

  • Download URL: jonq-0.3.0.tar.gz
  • Upload date:
  • Size: 629.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.6.16

File hashes

Hashes for jonq-0.3.0.tar.gz
Algorithm Hash digest
SHA256 7fc7e79fb81724ab4ef5d986337c84d2622d3eec8a9f8dd2154f6e7b5bc4f858
MD5 aa03b6d0708c330c2ada7c0aca36da4e
BLAKE2b-256 64600d6e8345c61d21f81d931ec28447810f8bbce590ee7aaf904fb13e4b1384

See more details on using hashes here.

File details

Details for the file jonq-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: jonq-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 47.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.6.16

File hashes

Hashes for jonq-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 edbe8535c484fa3f579939242826f19e86a671bf6ea9142e530bd7421d22c49a
MD5 0db2209438468f77c260fc6c638f65d1
BLAKE2b-256 5133aacea323480513dc088b413516f69ce0b497b016b3e1453f130c2d0c3869

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page