Decision guide
Native JSON flags or jc-rs?
Prefer a command's documented, stable JSON mode when your supported versions have one. On Linux that means starting with ip -j, lsblk -J, and journalctl -o json. Use jc-rs when formatted text is the real interface you must consume, or when old and mixed systems cannot offer the native mode.
By Oleg Sotnikov · Published and checked
In both branches, pin the invocation and test the fields. JSON guarantees syntax, not a permanent application schema.
Use the closest structured interface to the source
Native JSON is produced before a command lays data out for a terminal. It does not depend on translated column headings, padding, tree-drawing characters, or the current screen width. Skipping that presentation layer removes an entire class of parsing failures and one process from the pipeline.
The word native is not enough by itself. Prefer a mode documented by the command, available across the versions you support, and exercised by a schema test. Experimental JSON, an undocumented switch, or fields that change between installed releases still need compatibility work.
Documented native JSON
The producer exposes the records and fields you need.
Documented machine format
JSON is absent, but the tool offers stable pairs, null delimiters, or another formal mode.
jc-rs parser
Human-readable output is unavoidable and a parser exists for that exact command shape.
Purpose-built adapter
You own a narrow text contract that no existing parser covers.
Native wins
Use ip -j for addresses and routes
The iproute2 -j option asks the producer for JSON; -p only pretty-prints that JSON for a person. Keep compact output in pipelines and let jq choose the fields.
ip -j address show |
jq 'map({
ifname,
mtu,
addresses: [.addr_info[]? | {family, local, prefixlen}]
})'ip -j route show default |
jq 'map({gateway, dev, metric: (.metric // 0)})'Do not pipe ip -j into jc-rs. It is already JSON. For an older host where text from ip route is the only available artifact, use the ip route parser against that text instead.
Native wins
Use lsblk -J, and name every column
lsblk -J returns a JSON object with a blockdevices array. The util-linux manual explicitly warns that default output can change, including defaults selected by convenience options. For a script, JSON plus an implicit column list is only half a contract.
lsblk --json --tree \
--output NAME,TYPE,SIZE,MOUNTPOINTS |
jq '.blockdevices'Request --tree when hierarchy matters, and keep NAME in the selected columns. Otherwise the presence of nested children is not a safe assumption. If you must process saved table output from a host without JSON support, the jc-rs lsblk parser converts the familiar columns and adds typed fields such as byte counts.
Native wins
journalctl -o json is already a record stream
Journal entries are structured before they are rendered as short log lines. The json output mode writes one JSON object per line, so it is NDJSON rather than one surrounding array. jq accepts the sequence directly.
journalctl --unit=sshd.service \
--since='1 hour ago' \
--no-pager \
--output=json \
--output-fields=MESSAGE,PRIORITY,_SYSTEMD_UNIT |
jq -c 'select((.PRIORITY | tonumber) <= 3)'Keep it line-delimited for a streaming consumer. To build one bounded array, use jq -s '.' after applying a sensible time or row limit. The distinction is covered in the NDJSON, JSONL, and JSON guide.
Do not render the journal as short output and parse it back into fields. Use the journal's own JSON while you still have access to the source. The syslog parser is for actual syslog records, including exported files and streams. It is not a substitute for journalctl's structured mode.
Where jc-rs is the right boundary
A schema-aware text parser is useful when the text cannot be avoided. That happens more often than a greenfield script suggests: old appliances, support bundles, captured incident output, and commands whose maintainers expose only a terminal format are all common in production environments.
| Input you actually have | Use | Reason |
|---|---|---|
| Current ip address or route state | ip -j … | The producer owns a documented JSON mode. |
| Current block-device inventory | lsblk -J -o … | Native JSON plus explicit columns is the strongest contract. |
| Current systemd journal entries | journalctl -o json | It retains journal fields before display formatting. |
| Saved legacy lsblk table | jc-rs --lsblk | You cannot rerun the producer, so parse the artifact you have. |
| systemctl unit-list table | jc-rs --systemctl | The matching parser gives the known table a typed JSON boundary. |
| ps, df, or ss text from supported systems | matching jc-rs parser | Use one parser per command grammar; do not infer arbitrary columns. |
LC_ALL=C SYSTEMD_COLORS=0 \
systemctl --all --no-pager --full --plain |
jc-rs --systemctl |
jq 'map(select(.active == "failed"))'The command, options, locale, and parser name together define the input contract. See how to test command-output parsers across locale, width, and version changes.
JSON syntax is not your schema contract
Switching to native JSON prevents column-splitting bugs, but it does not promise that a field will exist forever or keep the same type. Record the producer version, request explicit fields where possible, and assert the minimum shape your consumer needs.
lsblk --json --tree --output NAME,TYPE,SIZE,MOUNTPOINTS |
jq -e '
select(
(.blockdevices | type == "array") and
all(.blockdevices[];
(.name | type == "string") and
(.type | type == "string") and
(.mountpoints | type == "array")
)
)
' > block-devices.jsonRun that assertion in CI against every supported distribution image. When you choose jc-rs, make the same kind of assertion against the fixture-derived schema shown on its parser page. The JSON producer changes, but the consumer still deserves a checked contract.
Upstream references
Flag behavior above is grounded in the upstream manuals: the iproute2 ip(8) manual, util-linux lsblk(8) manual, and systemd journalctl documentation. jc-rs behavior is checked against its repository fixtures.