Compared with jc and jo

On this page

Two established tools cover most of what jz does. jc converts the output of commands, file formats and strings to JSON. jo builds JSON from shell arguments. jz does a part of each: COMMAND | jz overlaps with jc, and jz new overlaps with jo. This page lists the differences as they stand and does not rank the tools. Each one is the better choice for some jobs.

The versions compared are jc 1.25.7 (released 2026-06-18, MIT licence), jo 1.9 (released 2022-11-04, GPL-2.0-or-later) and jz v0.9.0 (released 2026-09-22, MIT licence). jc has been developed since 2019 and jo since 2016. jz’s first commit was in September 2026. jc and jo are packaged by the major Linux distributions and Homebrew. jz is installed with go install, a Homebrew tap, the AUR, or the packages on its release page. No code or fixture from either tool is copied into this repository.

In short #

  • Use jc when you need a Python library, the Ansible filter, YAML output, line slicing, or input jz does not read: XML, TOML, plist and X.509 files; strings such as URLs, JWTs, timestamps and IP addresses; commands jz has no definition for, such as traceroute, iptables, dmidecode, wg, ufw, acpi, mdadm, ntpq, pacman, find, finger and zpool status; and more /proc files. When jz has to detect the format of a short output, jc was faster on one core and held less memory in the benchmarks below.
  • Use jo when you need the smallest and fastest program for building JSON, type guessing (n=1 becomes a number and a=$(jo b=1) nests an object without extra syntax), base64 file contents, or adding keys to an existing JSON document.
  • Use jz when you want the format detected from the text instead of named, input that does not fit refused rather than read partly, parsers defined in YAML without code, or command output, data files and argument-built JSON from one binary.

What jsonize does not do #

  • It does not query, sort, aggregate, or reshape JSON. Use jq for that.
  • It does not replace a command’s native JSON output. Prefer native output when available.
  • It does not parse arbitrary free-form text. Input must match a known or explicitly defined format.
  • It does not guess when input is ambiguous. Specify --parser, or conversion fails.
  • It does not silently discard input that it cannot parse.
  • It does not fetch parser definitions or access the network during conversion.
  • It does not invoke a shell for jz run; shell syntax is interpreted only when you explicitly run a shell.

A definition can still name lines that are not data, such as headings, total lines, comments and the df: messages printed before a table. Those lines are left out of the JSON without a message. --explain counts them.

Command output: jc and jz #

jc 1.25.7jz v0.9.0
Choosing the parsernamed on every call (jc --df), or taken from the command name (jc df -h)detected from the text; --parser narrows it, and some generic formats are read only when named
What is read236 parsers, 19 of them streaming versions of others: commands, /proc files, file formats and strings758 definitions under 280 names: commands, plus /etc and /proc files and the generic csv, ini, kv and table layouts; no string parsers
Commands only one of the two readstraceroute, iptables, dmidecode, wg, ufw, acpi, mdadm, ntpq, pacman, find, finger, zpool status, …systemd-* tools, lsfd, lsns, journalctl, sar, docker, sensors, smartctl, …
The two countsone jc parser can cover several layouts of a commandone jz definition reads one layout, so the two counts are not comparable
File formatsCSV, TSV, INI, YAML, TOML, XML, plist, X.509, and moreCSV, TSV, LTSV, JSON, JSON Lines and YAML, also gzip or bzip2 compressed; INI with --parser ini; plain text as one string, lines, or NUL-separated records
Input that does not fit the parserdepends on the parser: many return an empty or partial result with exit status 0 (echo hello world | jc --df prints []), others stop with exit status 100 (--uptime, --date)nothing on standard output, exit status 3 or 4, and the reason on standard error
Rounded sizes (df -h prints 13G)converted to a byte countkept as the string printed
Values without conversion-r / --raw--raw
Running the commandjc COMMAND, without a shell; the exit status is the command’s, plus 100 when jc failsjz run COMMAND, without a shell, under LC_ALL=C; a non-zero status of the command is passed on, otherwise jz’s own
Streaming19 streaming parsers (--ls-s, --ping-s, …), JSON Lines output; -qq records parse errors in the output and continues--stream for most formats, JSON Lines output; the first record that cannot be read ends the stream with exit status 3
Output formatsJSON, YAML (-y), colour, --meta-out metadataJSON only
Selecting part of the inputline slices (jc 4:15 --df)none; --extract and --exclude keep or drop keys of each record
Diagnostics-d for debug output--explain names the definition chosen and why the others were rejected
Adding a parsera Python module in the plugin directorya YAML definition with captured fixtures, checked by jz test; no Go needed
LibraryPython (jc.parse('df', text))Go (pkg/registry, pkg/selector, pkg/engine)
RuntimePython 3.6 or later with ruamel.yaml, xmltodict and Pygments, or a prebuilt binary from its release pagea single static binary
IntegrationsAnsible filter plugin, FortiSOAR connectornone

The output of the two differs in key names, in which values are converted, and in which texts each accepts. Coverage has a fixture-by-fixture comparison. It uses jz’s own fixtures, which were chosen for jz’s definitions, so its counts favour jz and do not measure how often either tool is right on text in general.

JSON from arguments: jo and jz new #

jo 1.9jz new (v0.9.0)
Value typesguessed: a number, true, false, null, and a JSON object or array become JSON values; -s, -n, -b force a type per word, and -B turns off the true/false/null guessnot guessed: key=value is always a string, key:=json is always JSON
Nesting the output of another calla=$(jo b=1)a:=$(jz new b=1)
Booleanskey@1, key@tkey:=true
Empty value (key=)null, or omitted with -n""
Nesting by patha[b]=1, or -d. for a.b=1--path /a/b=1 (a JSON Pointer)
Arrays-a and key[]=value--array and key[]=value
A file as the value@file text without its final line break, %file base64, :file JSON=@file text without its final line break, --text-file with every line ending kept, :=@file JSON or a data file read by its extension; no base64
Standard inputread as words, one per line, or as a document with -f -as a value with key=@-, key:=@- or --text-file key=-; --each makes one document per input line
Adding to an existing document-f file loads a JSON object or array and adds the words to itnot available; key:=@file puts the document under a key
The same key twicewritten twice; -D keeps the last valuerefused with exit status 2
Pretty output-p-p
Program size, stripped (Linux amd64)39 KB, linked to the C library15 MB, static, the parsers included

The difference in value types is the one most likely to matter:

$ jo version=1.10 zip=007 enabled=true note=
{"version":1.1,"zip":7,"enabled":true,"note":null}

$ jz new version=1.10 zip=007 enabled:=true note=
{"version":"1.10","zip":"007","enabled":true,"note":""}

jo’s guess saves writing :=. A value that looks like a number loses its spelling unless its type is forced (jo -- -s zip=007).

Benchmarks #

These are the timings of one process per call, measured with himorime from bench/compare/himorime.yaml and written into this page by make bench-docs. They measure time, CPU time and peak memory only, not whether the JSON is correct.

jz is built as the release is built (CGO_ENABLED=0, -trimpath, -ldflags "-s -w") from the commit named under the tables. jc 1.25.7 is installed from PyPI; its prebuilt binaries were not measured. jo 1.9 is built with meson and stripped.

The df -h and ps aux input is captured on the machine that runs the suite, and ps aux is repeated to 100,000 rows. The CSV has 100,000 generated rows, a third of them with a quoted field that holds a comma. Before measuring, a setup step checks that both tools return the same number of records, and that jz new and jo build the same JSON.

jz has three ways to read command output, and they cost different amounts. Detecting the format (COMMAND | jz, jz-detected below) reads all built-in definitions, because any of them could fit the text. Naming the parser (--parser NAME, jz-parser) and running the command (jz run COMMAND) read only the definitions that answer to that name. jc always reads only the parser it is given.

jz loads its definitions and reads large input on several threads, and jc runs on one. Each case is measured twice: with every core available, and pinned to one core with taskset -c 0 (the rows ending in “one core”). The pinned figure is closer to what a machine or container limited to one core sees. Starting taskset is part of the pinned figures, which is most of the pinned time of jo and jz new.

In the tables, Relative is a command’s median time divided by that of the jz command its case compares against: jz-parser for command output, and the only jz command otherwise. CPU time is user plus system time. Peak RSS is the largest resident set size of the process.

jz, jc and jo #

One process per call on the same input.

Latency #

BenchmarkCommandMedianP95MeanStddevMinMaxRunsRelative
start-upjz1.71ms3.38ms1.90ms735.78µs963.77µs4.29ms301.00x
start-upjc35.94ms58.28ms38.61ms10.51ms28.44ms59.43ms3021.05x
start-upjo608.47µs970.29µs636.54µs208.95µs348.27µs1.08ms300.36x
start-up, one corejz1.50ms2.19ms1.54ms344.32µs948.72µs2.56ms301.00x
start-up, one corejc29.95ms36.57ms31.14ms3.40ms29.28ms46.56ms3019.99x
start-up, one corejo878.11µs1.26ms909.18µs273.59µs492.37µs1.63ms300.59x
df -hjz-detected31.58ms34.71ms31.87ms1.77ms28.34ms35.42ms305.02x
df -hjz-parser6.29ms7.26ms6.27ms604.93µs5.26ms7.56ms301.00x
df -hjc36.23ms43.27ms37.50ms4.23ms33.51ms53.84ms305.76x
df -h, one corejz-detected54.80ms106.78ms63.22ms18.25ms52.07ms115.30ms309.10x
df -h, one corejz-parser6.02ms9.77ms6.40ms1.71ms4.62ms12.87ms301.00x
df -h, one corejc35.04ms62.42ms39.34ms9.73ms33.73ms68.42ms305.82x
running df -hjz7.82ms9.36ms7.77ms965.89µs5.61ms9.51ms301.00x
running df -hjc72.12ms77.91ms72.43ms3.57ms65.06ms80.37ms309.22x
running df -h, one corejz7.27ms7.94ms7.25ms609.08µs5.80ms8.97ms301.00x
running df -h, one corejc67.68ms71.14ms68.50ms4.28ms64.89ms89.75ms309.32x
ps aux, 100k rowsjz-detected208.03ms237.46ms211.22ms17.29ms197.07ms285.80ms301.17x
ps aux, 100k rowsjz-parser178.47ms195.61ms183.63ms24.66ms164.35ms307.38ms301.00x
ps aux, 100k rowsjc436.13ms448.34ms437.50ms7.88ms428.12ms467.14ms302.44x
ps aux, 100k rows, one corejz-detected268.21ms324.48ms278.04ms38.59ms260.22ms462.16ms301.33x
ps aux, 100k rows, one corejz-parser201.08ms203.90ms207.70ms36.95ms198.65ms403.21ms301.00x
ps aux, 100k rows, one corejc439.32ms507.85ms449.19ms35.55ms430.70ms611.56ms302.18x
CSV, 100k rowsjz91.79ms100.60ms92.70ms6.62ms84.26ms119.32ms301.00x
CSV, 100k rowsjc145.63ms151.14ms145.35ms3.20ms140.35ms152.25ms301.59x
CSV, 100k rowsjz-stream109.11ms119.81ms110.22ms5.27ms104.31ms125.69ms301.19x
CSV, 100k rowsjc-stream262.34ms285.53ms268.65ms27.58ms255.35ms409.00ms302.86x
CSV, 100k rows, one corejz107.50ms108.88ms107.62ms2.84ms100.46ms120.20ms301.00x
CSV, 100k rows, one corejc142.52ms165.98ms147.21ms14.83ms139.59ms216.84ms301.33x
CSV, 100k rows, one corejz-stream103.99ms114.29ms105.26ms4.18ms102.68ms121.21ms300.97x
CSV, 100k rows, one corejc-stream261.35ms272.12ms267.76ms27.95ms257.67ms414.31ms302.43x
object from argumentsjz-new1.15ms1.33ms1.14ms102.52µs957.39µs1.34ms301.00x
object from argumentsjo326.64µs379.16µs327.16µs35.19µs270.84µs389.29µs300.29x
object from argumentsjz-new-path1.15ms1.42ms1.16ms213.13µs805.36µs2.03ms301.01x
object from argumentsjo-nested335.57µs487.78µs346.83µs85.78µs202.82µs689.85µs300.29x
object from arguments, one corejz-new922.29µs996.52µs921.03µs59.63µs764.97µs1.04ms301.00x
object from arguments, one corejo603.42µs644.10µs593.89µs44.56µs442.21µs667.70µs300.65x
object from arguments, one corejz-new-path900.64µs1.13ms922.00µs114.18µs734.04µs1.33ms300.98x
object from arguments, one corejo-nested607.53µs655.35µs598.37µs51.52µs443.21µs686.75µs300.66x

Relative is the median divided by the baseline command’s median, or by the fastest command’s.

Throughput #

BenchmarkCommandMedianMeanMinP95
ps aux, 100k rowsjz-detected480.70k records/s475.90k records/s349.90k records/s506.09k records/s
ps aux, 100k rowsjz-parser560.31k records/s550.81k records/s325.33k records/s591.14k records/s
ps aux, 100k rowsjc229.29k records/s228.64k records/s214.07k records/s233.33k records/s
ps aux, 100k rows, one corejz-detected372.84k records/s364.07k records/s216.37k records/s378.67k records/s
ps aux, 100k rows, one corejz-parser497.32k records/s489.33k records/s248.01k records/s502.15k records/s
ps aux, 100k rows, one corejc227.62k records/s223.69k records/s163.52k records/s231.09k records/s
CSV, 100k rowsjz1.09M records/s1.08M records/s838.06k records/s1.18M records/s
CSV, 100k rowsjc686.66k records/s688.33k records/s656.80k records/s710.07k records/s
CSV, 100k rowsjz-stream916.55k records/s909.16k records/s795.63k records/s956.72k records/s
CSV, 100k rowsjc-stream381.19k records/s374.86k records/s244.50k records/s389.91k records/s
CSV, 100k rows, one corejz930.26k records/s929.75k records/s831.98k records/s944.40k records/s
CSV, 100k rows, one corejc701.67k records/s684.26k records/s461.17k records/s713.66k records/s
CSV, 100k rows, one corejz-stream961.61k records/s951.34k records/s825.04k records/s972.26k records/s
CSV, 100k rows, one corejc-stream382.63k records/s376.09k records/s241.37k records/s387.13k records/s

Throughput is the declared work divided by the latency of each run.

CPU #

BenchmarkCommandUserSystemTotalTotal p95Utilization
start-upjz866.00µs1.27ms1.82ms2.55ms108.1%
start-upjc29.57ms5.05ms35.84ms57.18ms99.7%
start-upjo0ns419.00µs545.00µs853.45µs90.8%
start-up, one corejz0ns1.30ms1.44ms2.12ms96.1%
start-up, one corejc26.59ms4.51ms29.88ms36.47ms99.7%
start-up, one corejo0ns501.50µs759.00µs1.14ms93.1%
df -hjz-detected156.66ms28.22ms185.89ms205.58ms589.1%
df -hjz-parser5.42ms3.46ms9.08ms10.64ms140.0%
df -hjc31.18ms5.02ms36.05ms43.15ms99.7%
df -h, one corejz-detected49.15ms6.02ms54.61ms105.77ms99.8%
df -h, one corejz-parser3.45ms2.65ms5.87ms9.20ms98.1%
df -h, one corejc30.21ms5.92ms34.80ms62.12ms99.5%
running df -hjz6.07ms4.51ms10.48ms12.13ms133.9%
running df -hjc60.70ms11.79ms71.86ms77.53ms99.5%
running df -h, one corejz4.04ms2.80ms6.92ms7.65ms95.7%
running df -h, one corejc57.44ms9.06ms66.45ms69.76ms98.4%
ps aux, 100k rowsjz-detected402.89ms84.55ms483.37ms534.87ms230.6%
ps aux, 100k rowsjz-parser213.60ms56.84ms269.22ms318.72ms146.2%
ps aux, 100k rowsjc383.85ms53.51ms435.51ms448.06ms99.9%
ps aux, 100k rows, one corejz-detected214.98ms53.98ms267.99ms323.08ms99.9%
ps aux, 100k rows, one corejz-parser153.22ms47.71ms200.89ms203.67ms99.9%
ps aux, 100k rows, one corejc385.36ms54.04ms439.01ms506.15ms99.9%
CSV, 100k rowsjz118.44ms23.49ms142.58ms155.66ms154.7%
CSV, 100k rowsjc121.39ms23.90ms145.35ms150.95ms99.9%
CSV, 100k rowsjz-stream113.49ms16.79ms130.75ms143.20ms118.5%
CSV, 100k rowsjc-stream255.93ms7.00ms262.12ms285.33ms99.9%
CSV, 100k rows, one corejz88.77ms18.14ms107.28ms108.65ms99.8%
CSV, 100k rows, one corejc122.36ms21.05ms142.36ms164.56ms99.9%
CSV, 100k rows, one corejz-stream95.14ms8.97ms103.84ms114.07ms99.8%
CSV, 100k rows, one corejc-stream254.94ms6.99ms261.04ms271.65ms99.9%
object from argumentsjz-new0ns1.22ms1.25ms1.44ms108.4%
object from argumentsjo277.50µs0ns299.50µs349.55µs91.9%
object from argumentsjz-new-path0ns1.16ms1.22ms1.55ms108.8%
object from argumentsjo-nested301.50µs0ns306.50µs452.55µs91.8%
object from arguments, one corejz-new0ns845.50µs879.50µs950.95µs95.6%
object from arguments, one corejo0ns544.00µs568.00µs605.65µs94.7%
object from arguments, one corejz-new-path0ns729.50µs866.00µs1.08ms95.7%
object from arguments, one corejo-nested412.00µs0ns577.50µs607.55µs94.3%

CPU values are medians over runs of the process tree. Utilization is CPU time divided by wall-clock time; above 100% means more than one CPU was busy. Process tree: the command plus every descendant its parent waited for (rusage); a descendant left running or reaped by init is not counted.

Memory #

BenchmarkCommandPeak RSS (median)Peak RSS (max)
start-upjz≤ 7.57MiB≤ 7.57MiB
start-upjc19.41MiB19.72MiB
start-upjo≤ 7.54MiB≤ 7.54MiB
start-up, one corejz≤ 7.57MiB≤ 7.57MiB
start-up, one corejc19.70MiB19.77MiB
start-up, one corejo≤ 7.57MiB≤ 7.57MiB
df -hjz-detected30.42MiB32.09MiB
df -hjz-parser14.86MiB15.61MiB
df -hjc20.95MiB21.41MiB
df -h, one corejz-detected30.36MiB31.86MiB
df -h, one corejz-parser14.11MiB14.11MiB
df -h, one corejc21.39MiB21.57MiB
running df -hjz15.04MiB15.61MiB
running df -hjc27.47MiB28.10MiB
running df -h, one corejz14.36MiB14.36MiB
running df -h, one corejc28.06MiB28.12MiB
ps aux, 100k rowsjz-detected215.51MiB224.36MiB
ps aux, 100k rowsjz-parser194.89MiB205.61MiB
ps aux, 100k rowsjc198.40MiB199.22MiB
ps aux, 100k rows, one corejz-detected230.48MiB238.86MiB
ps aux, 100k rows, one corejz-parser203.11MiB207.36MiB
ps aux, 100k rows, one corejc216.51MiB216.65MiB
CSV, 100k rowsjz65.87MiB68.29MiB
CSV, 100k rowsjc85.29MiB85.83MiB
CSV, 100k rowsjz-stream11.08MiB11.98MiB
CSV, 100k rowsjc-stream19.77MiB20.29MiB
CSV, 100k rows, one corejz79.11MiB79.36MiB
CSV, 100k rows, one corejc85.93MiB86.07MiB
CSV, 100k rows, one corejz-stream10.35MiB10.36MiB
CSV, 100k rows, one corejc-stream20.22MiB20.29MiB
object from argumentsjz-new≤ 9.65MiB≤ 9.65MiB
object from argumentsjo≤ 9.63MiB≤ 9.63MiB
object from argumentsjz-new-path≤ 9.64MiB≤ 9.64MiB
object from argumentsjo-nested≤ 9.65MiB≤ 9.65MiB
object from arguments, one corejz-new≤ 9.61MiB≤ 9.61MiB
object from arguments, one corejo≤ 9.62MiB≤ 9.62MiB
object from arguments, one corejz-new-path≤ 9.62MiB≤ 9.62MiB
object from arguments, one corejo-nested≤ 9.63MiB≤ 9.63MiB

Peak RSS is a resident set size, not the heap size of a language runtime. Peak RSS is the largest peak of any single process of the tree (rusage ru_maxrss), not the combined memory of processes running at the same time. ≤ marks a peak RSS at or below what the process starting the command already used; the command used at most that much.

Measured with himorime v0.1.2 on linux/amd64, AMD RYZEN AI MAX+ 395 w/ Radeon 8060S (32 logical CPUs), head 2a3a868d8e2e, seed 3688326356035415.

  • go: go version go1.26.6 linux/amd64
  • jc: jc version: 1.25.7
  • jo: jo 1.9
  • jq: jq-1.8.1
  • taskset: taskset from util-linux 2.41.3

Reading the figures #

  • Starting the process: jz printed its version in about 1.5 to 1.7 ms, jo in about 0.6 to 0.9 ms, and jc in 30 to 36 ms. Most of jc’s time on short input is starting Python and loading jc.
  • With the parser named, or with jz running the command, jz read a short df -h report in about 6 to 8 ms and jc in 35 to 72 ms.
  • When jz detects the format of a short output, jc was faster on one core (35 ms against 55 ms) and held less memory (21 MiB against 30 MiB). On all cores jz was slightly faster, using about five times the CPU time: detection reads every built-in definition and compiles the signatures it checks.
  • On 100,000 rows of ps aux and on a whole CSV, jz was faster than jc on both core settings, detected or named: about 2.2 to 2.4 times for ps aux and 1.3 to 1.6 times for the CSV. It held about the same memory as jc for ps aux and less for the CSV.
  • Streaming a CSV, jz was about 2.5 times as fast as jc on both core settings and held about half the memory.
  • jo was about 3.5 times as fast as jz new on all cores and about 1.5 times pinned to one core. Both finish in about a millisecond, so the difference matters when a script starts the program thousands of times. Their peak memory is below what this measurement resolves (the rows marked ≤): both used less than the process that started them.
  • These results come from one machine and one run of the suite, and jc’s figures depend on the Python version. Run make bench-docs to measure another machine.