leafdump
Make nested data greppable. Every line is one value and the complete path to
it, so a service configuration, an API response or a wall of log records
becomes something grep, less, cut and awk already know what to do with.
$ leafdump config.json
ROOT.{version}."3"
ROOT.{configurePresets}.0.{name}."default"
ROOT.{configurePresets}.0.{hidden}.true
ROOT.{configurePresets}.0.{cacheVariables}.{CMAKE_BUILD_TYPE}."Release"
That matters most in the case those tools otherwise handle worst: a document
that arrives as one enormous line with no newlines in it, where grep matches
the whole file and a pager shows you a wall.
$ grep name minified.json | wc -l # 1 — the whole document matched
$ leafdump minified.json | grep name # one line per hit, with its path
It is built for the question that comes up in front of a machine that is misbehaving: what is actually in this file, where does that setting live, and which of these hosts disagrees with the others. Answering it needs no query language, no editor and no scripting — only the filters already in your fingers — and the program itself needs nothing but Python.
Two further modes share the same reader. One converts between serialisation formats, so a file in something nothing local can open becomes one that everything can. The other merges several documents into a single structure, which is how a base configuration and its per-host overrides get compared or combined.
This is a different job from jq, and the two get along. jq is the tool for real queries, joins and transformations, at the price of a filter language you have to know well enough to write under time pressure. leafdump is for the other half: finding where a value lives, reading the shape of a document you have never opened, or grepping a directory of them.
Install
Python 3.11 or newer, and nothing else:
pipx install 'leafdump[all]' # + YAML, JSON5, TOON, TOML output, MessagePack, CBOR, NestedText
pip install leafdump # core: every pseudocode dump, JSON, JSONL, repr, TOML input
pip install 'leafdump[yaml]' # or pick individual formats
For a machine with no Python on it, make binary compiles the whole thing —
interpreter, package and codecs — into one self-contained executable:
$ make binary && ./bin/leafdump --version
leafdump 0.2.0
formats enabled: json, jsonl, repr, pseudocode, toml, yaml, json5, toon, msgpack, cbor, nestedtext
notations: perl, python, javascript, cpp, go, rust, ruby, php, lua, r, jq, jsonpath, jsonpointer, dotted, shell
Nuitka is not a dependency and is not downloaded until that target runs.
INSTALL.md has the rest: every extra, installing from source, running straight from a checkout, the single-file binary, shell completion and the manpage.
The core has no dependencies. Every other format is optional and is hidden
from --help until its package is installed:
$ leafdump --to cbor data.json
leafdump: error: support for writing 'cbor' is not installed.
pip install cbor2 (or: pip install 'leafdump[cbor]')
Pseudocode notations
The path dump is rendered from a template: a table of literal strings
saying how one leaf is spelled as a line of some language. Fifteen are built
in, and --template (-T) picks one:
$ leafdump --template go hosts.json
ROOT["hosts"][0]["name"] = "web-01"
ROOT["hosts"][0]["tls"] = true
ROOT["hosts"][0]["tags"] = []any{}
| Template | One leaf, rendered |
|---|---|
perl (default) |
ROOT.{hosts}.0.{name}."web-01" |
python |
ROOT['hosts'][0]['name'] = 'web-01' |
javascript |
ROOT.hosts[0].name = "web-01"; |
cpp |
ROOT["hosts"][0]["name"] = "web-01"; |
go |
ROOT["hosts"][0]["name"] = "web-01" |
rust |
ROOT["hosts"][0]["name"] = json!("web-01"); |
ruby |
ROOT["hosts"][0]["name"] = "web-01" |
php |
$ROOT["hosts"][0]["name"] = "web-01"; |
lua |
ROOT["hosts"][1]["name"] = "web-01" |
r |
ROOT[["hosts"]][[1]][["name"]] <- "web-01" |
jq |
.["hosts"][0]["name"] = "web-01" |
jsonpath |
$.hosts[0].name = "web-01" |
jsonpointer |
/hosts/0/name = "web-01" |
dotted |
ROOT.hosts.0.name=web-01 |
shell |
ROOT[hosts.0.name]='web-01' |
leafdump --list-templates prints that table from the live catalogue, custom
templates included. Every built-in is also an output format of its own name,
so -t go and -T go are the same thing.
Lua and R count from 1, and the templates know it — the first array
element really is [1] there. The languages that quote differently escape
differently too: each template names a quoting style, and the style does the
escaping.
Three notations are worth calling out:
perl is the default, and the one this tool was written to keep (see
History). Values are quoted but untyped, so 1 and "1" are
indistinguishable; --perl-compat restores the original's quirks exactly.
python escapes with repr(), so quoting is exact, types survive, empty
containers are representable, and the lines replay to rebuild the structure.
It is the better default for anything but grep.
dotted quotes nothing at all, which makes it the friendliest thing to
hand to cut and awk — and the most ambiguous. Pair it with -e.
Writing your own
A template is a JSON (or TOML) file of literal strings. Nothing in it is imported, evaluated or executed — the loader reads data and rejects any field or placeholder it does not recognise.
$ leafdump --help-template # every field, placeholder and quoting style
$ leafdump --help-template go # one notation, field by field
$ leafdump --dump-template go > kotlin.json # a starting point to edit
Usually only a few lines differ from something built in, so base inherits
the rest:
{
"base": "javascript",
"name": "kotlin",
"summary": "Kotlin map/list assignments",
"line": "%p = %v",
"bare": "",
"empty_map": "mapOf<String, Any?>()",
"empty_seq": "listOf<Any?>()"
}
$ leafdump --template ./kotlin.json hosts.json
ROOT["hosts"][0]["name"] = "web-01"
The placeholders are %r (root or a raw key), %c (the path chain), %s (a
segment as a literal), %p (the finished path) and %v (the finished
value). Drop the file into ~/.config/leafdump/templates/ — or any
directory named in $JSON_DUMP_TEMPLATES — and it becomes --template kotlin. Built-in names always resolve first, so nothing can quietly redefine
perl for a script that expected it.
contrib/templates/ has three worked examples: inheriting
with base, a flat TSV notation written from scratch, and one that emits SQL
INSERT statements with a header.
Converting
$ leafdump --to yaml config.json
$ leafdump --to json --compact data.msgpack
$ producer | leafdump --from msgpack --to jsonl
leafdump --list-formats shows what is available; --help-format NAME
explains one format's dependencies and round-trip caveats, including for
formats that are not installed.
Merging
Multiple inputs merge into one structure:
$ leafdump --to json base.json site.json local.json
$ leafdump --to json --dedup --list-merge union a.json b.json
$ leafdump --to json --merge-strategy collect a.json b.json # keep conflicts
$ leafdump --to json --wrap-key stem */settings.json # key by filename
Deduplication compares by value and type, so 1, 1.0 and true stay
distinct even though Python considers them equal.
History
leafdump is a direct descendant of json_dump.pl, a Perl script I wrote and
used constantly for several years — and would probably still be using, if Perl
were still as ubiquitous on a fresh machine as it once was. The rewrite exists
because that assumption stopped holding, not because the notation needed
fixing: the default perl output is the same notation, for the same reason.
The Perl script itself is retired, but the comparison is not: its output for
every document in contrib/ is frozen in the repository, and every change is
checked against it line for line. That check used to skip wherever perl was
absent; now it runs everywhere. --perl-compat reproduces the original byte for
byte, quirks and all: null renders as ""
rather than undef, empty maps and arrays produce no line at all, and -e
escapes only CR and LF. The flags -r, -m and -e kept their original
meanings.
Documentation
man leafdumpleafdump --help- INSTALL.md — extras, source installs, completion, manpage
- VERSIONING.md — what a major bump means, and what is promised
- ROADMAP.md — planned work: SQL-style filtering, an interactive shell, out-of-core storage for structures larger than RAM, Ibis integration, and publication to document stores.
- AUTHORS.md — who wrote it
Shell completion
Completion for bash, fish and zsh lives in contrib/completions/.
Each queries leafdump -L --porcelain and --list-templates --porcelain, so
the candidates offered always match the optional packages actually installed and
the templates actually on your search path — no hard-coded lists to drift.
See INSTALL.md for where to put them.
Tests
python3 -m unittest discover -s tests -v
The suite runs the original Perl script alongside the rewrite and compares the
output line for line when perl is available, and round-trips every installed
format.
The source distribution on PyPI carries the suite and everything it reads, so the same command works from an unpacked tarball with no clone — see INSTALL.md if you are packaging this for a distribution.
Development
make is the front end. The git hooks and the GitHub workflows call the same
targets, so a gate means one thing everywhere.
$ make # every target, with examples
$ make tools hooks # one-time setup in a fresh clone
$ make precommit # format, lint, the dependency-free check, tests
$ make check # everything CI runs, in CI's order
Those targets build throwaway virtualenvs: one for the pinned linters, and one
per run of make test-isolated, which is what proves the package still works
installed with no extras at all. Building them needs a python3 whose venv
module can bootstrap pip. Several distributions ship that separately
(python3-venv on Debian and Ubuntu, python3-pip on Fedora), so a minimal
container image usually has to install it before make check will run.
uv covers the same need without it, and make
prefers uv whenever it is on PATH.
CI runs the tests on Python 3.11 through 3.14, in two installations each:
with every extra, and with none — the second being the one that catches an
optional import escaping to module scope, which works fine on every machine
that has the package. Security coverage is ruff's bandit rules and CodeQL over
the source, pip-audit over the optional dependency set, and zizmor over the
workflows themselves. See SECURITY.md for the threat model.
License
Copyright 2026 Jeremy Melanson.
Licensed under the Apache License, Version 2.0. You may not use this software except in compliance with the License. Unless required by applicable law or agreed to in writing, it is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Metadata
Release files for leafdump 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| leafdump-1.0.0.tar.gz | 209.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| leafdump-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 270.9 kB
Release files / leafdump-1.0.0.tar.gz
| Download URL | leafdump-1.0.0.tar.gz |
|---|---|
| Size | 209.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b13ccd5230e177a4a055c746b5d36fbce3159ebde501b232019ab45a61784e5d
|
|
BLAKE2b-256 checksum How to use checksums |
00fd9bb61020d45af1dc72f2c8404a9a93196a12978abe7f2268418339cf8ad5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / leafdump-1.0.0-py3-none-any.whl
| Download URL | leafdump-1.0.0-py3-none-any.whl |
|---|---|
| Size | 61.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a59dcbb39ce55601627b6adb87f4f504169d6b32419ecd39e51dbf7198d5ae3a
|
|
BLAKE2b-256 checksum How to use checksums |
1f59a3e37e57a5da4ab23d4525276221aa4265a4226f82f208da2d1645b8016a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency log