Skip to content

fix: --json emits invalid JSON when a column is skipped by --only or --tree - #958

Open
VXNCXNX wants to merge 1 commit into
dalance:masterfrom
VXNCXNX:fix-json-trailing-comma
Open

fix: --json emits invalid JSON when a column is skipped by --only or --tree#958
VXNCXNX wants to merge 1 commit into
dalance:masterfrom
VXNCXNX:fix-json-trailing-comma

Conversation

@VXNCXNX

@VXNCXNX VXNCXNX commented Aug 15, 2026

Copy link
Copy Markdown

What's broken

--json emits invalid JSON whenever a column is skipped, which is what --only and --tree both do.

$ procs --only pid --json
[
{"PID": 1, },
{"PID": 2, },
$ procs --only pid --json | python3 -c "import sys,json; json.load(sys.stdin)"
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 2 column 12

--tree --json fails the same way, from the other end:

{, "PID": 1, ...}
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 2 column 2

Every consumer of --json is a program, so this is the one output mode where a stray comma is fatal rather than cosmetic.

Cause

The separator was decided by position among all columns:

for (j, c) in self.columns.iter().enumerate() {
    if c.visible && c.kind != ConfigColumnKind::Separator {
        line.push_str(&c.column.display_json(*pid));
        if j != len_column - 1 {
            line.push_str(", ");
        }
    }
}

while the field itself was only written for visible columns. Two ways to diverge:

  • --only leaves later columns invisible, so the last emitted field is not the last indexed one and still gets a trailing ", ".
  • Tree's display_json returns an empty string, so the branch runs, writes nothing, and contributes a separator anyway. That one lands at the front, giving {, "PID": ....

#839 fixed the --only/--tree panic and #763 taught this same loop to skip Separator columns, so the shape of the problem is known. What neither touched is the index the comma is keyed on, which is why the invalid output survived both.

The fix

Collect the fields that are actually emitted, drop the empty ones, and join(", "). A separator can then only appear between two real fields, which is the invariant that was missing.

After, all three modes are valid JSON, and the default procs --json is unchanged at the same 227 rows on this machine.

Verification

A unit test on the new json_object helper covering the empty case, one field, a field with empties on both sides, and two fields.

Reverting the change fails it:

test view::tests::json_object_skips_columns_without_json ... FAILED
  left: "{, \"PID\": 1, }"
 right: "{\"PID\": 1}"

End to end, before and after binaries built from this same checkout, piping each mode through a JSON parser:

              --only pid --json      --tree --json        --json
before        JSONDecodeError        JSONDecodeError      valid, 227 rows
after         valid                  valid                valid, 227 rows

cargo test --release is 14 passed, 0 failed. cargo clippy and cargo fmt --check are clean.

display_json decided separators by index over ALL columns but only emitted visible non-empty ones. --only and --tree thus left trailing or leading commas. Fixed by collecting emitted fields and joining.
@VXNCXNX
VXNCXNX force-pushed the fix-json-trailing-comma branch from 8b88c0f to bfa2c02 Compare August 24, 2026 13:45
@VXNCXNX

VXNCXNX commented Aug 24, 2026

Copy link
Copy Markdown
Author

Rebased onto current master, no conflicts.

On the new base: cargo test --bins -> 14 passed, cargo clippy --all-targets clean, cargo fmt --check clean.

Dropping the .filter(|x| !x.is_empty()) from json_object fails the new test as an assertion, which is the exact defect:

assertion `left == right` failed
  left: "{, \"PID\": 1, }"
 right: "{\"PID\": 1}"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant