# JSON output

> Every command's --json output is one envelope on stdout, with the same fields for every command. Read it from scripts, and follow long jobs with progress events.

## Get JSON from any command

Add `--json` to any command. stdout then holds exactly one JSON document, even when the command fails. Human messages go to stderr, and so do progress events when you ask for them.

```sh
census findings --safety safe --json
```

```json
{
  "schema_version": 5,
  "command": "findings",
  "status": "ok",
  "exit_code": 0,
  "errors": [],
  "warnings": [],
  "results": {
    "findings": [],
    "totals": {
      "safe": { "allocated_bytes": 0, "count": 0 },
      "review": { "allocated_bytes": 0, "count": 0 },
      "keep": { "allocated_bytes": 0, "count": 0 }
    },
    "truncated": 0
  }
}
```

Options can go before or after the command, so `census --json top` is the same as `census top --json`. Help text is always plain text, never JSON.

## The envelope

Every JSON document has these top-level fields:

| Field | Meaning |
| --- | --- |
| `schema_version` | The contract version. The current version is `5`. |
| `command` | The command as invoked, such as `top` or `clean`. The `dupes` alias reports `duplicates`. It is `null` when the arguments failed to parse before a command was known. |
| `status` | `ok`, `complete`, `partial`, `failed`, `not_found` or `cancelled`. |
| `exit_code` | The process's exit code. |
| `errors` | Failures, as `{code, message}` entries. Empty when `exit_code` is `0`, never empty otherwise. |
| `warnings` | Warning strings. |

Queries add `scan`, the saved scan they read, and `results`, which differs by command. Each command's page shows its `results`.

## Statuses

| Status | Exit code | Meaning |
| --- | --- | --- |
| `ok` | `0` | A query succeeded, or a cleanup deleted everything it could. Skipped items are normal. |
| `complete` | `0` | A scan read everything in its scope. |
| `failed` | `1` or `2` | A bad request (1) or an operational failure (2). |
| `partial` | `4` | Some folders couldn't be read, some items couldn't be fully deleted, or a maintenance task failed. |
| `not_found` | `7` | There is no saved scan, or the path or app isn't in it. |
| `cancelled` | `130` | Ctrl-C or `SIGTERM` stopped a long operation. |

[Exit codes and errors](https://diskcensus.com/docs/cli/errors.md) lists every error code.

## Paths

Paths and names in query results are `PathValue` objects, because a macOS file name doesn't have to be valid UTF-8:

| Field | Meaning |
| --- | --- |
| `display` | The path as text. Home folders appear as they are on your Mac, such as `/Users/me`. |
| `lossy` | `true` when `display` had to replace bytes that aren't valid UTF-8. |
| `bytes_hex` | The exact bytes, in hex. Present only when `display` loses them. |

```json
{ "display": "/Applications/Xcode.app/Contents", "lossy": false }
```

`review --json` is the exception: its `path` is a plain string, because selection files for `apply` use exactly those strings.

## Sizes

Sizes in results are whole bytes. `allocated_bytes` is space on disk, which is what deleting frees; `logical_bytes` is a file's length. Sizes you pass as options, such as `--min-size 500K`, take bytes or a number followed by `K`, `M` or `G`, in powers of 1000, as macOS shows sizes.

## Read JSON in a script

Check `exit_code` before reading `results`. These examples use `jq`:

```sh
# The five largest files, size then path
census top --files --limit 5 --json \
  | jq -r '.results.files[] | "\(.allocated_bytes)\t\(.path.display)"'

# Stop with census's own message when a command fails
out=$(census findings --json)
if [ "$(jq -r .exit_code <<<"$out")" -ne 0 ]; then
  jq -r '.errors[0].message' <<<"$out" >&2
  exit 1
fi
```

The same in Python:

```python
import json
import subprocess

proc = subprocess.run(
    ["census", "findings", "--safety", "safe", "--json"],
    capture_output=True,
    text=True,
)
doc = json.loads(proc.stdout)  # stdout is JSON even when the command fails
if doc["exit_code"] != 0:
    raise SystemExit(doc["errors"][0]["message"])

safe = doc["results"]["totals"]["safe"]
print(f"{safe['count']} safe items, {safe['allocated_bytes'] / 1e9:.1f} GB")
```

When stdout closes early, as in `census top | head -1`, census exits quietly with status 141 (`SIGPIPE`), like other Unix tools.

## Follow progress

Long jobs accept `--progress`. It writes one JSON record per line (NDJSON) to stderr, flushed as it goes, so stdout still holds only the result:

```sh
census scan --json --progress 2>progress.ndjson
```

`--progress` works with `scan`, `duplicates`, exports that include duplicates, any query with `--refresh`, `clean`, `review --refresh`, `apply`, `uninstall`, `maintain` with tasks, `optimize` and `empty-trash`. Each record starts with `event` and `elapsed_ms` (milliseconds since the command started), then the fields of its `phase`:

| Phase | Fields |
| --- | --- |
| `loading_cache` | None |
| `replaying_changes` | None |
| `walking` | `directories_read`, `files_seen`, `allocated_bytes` |
| `summarizing` | None |
| `saving` | None |
| `hashing` | `stage` (`starts` or `contents`), `files_done`, `files_total`, `bytes_read`, `bytes_total` |
| `deleting` | `items_done`, `items_total`, `bytes_freed`: once before the first item, then after each |
| `maintaining` | `task` (`null` in the last record), `tasks_done`, `tasks_total` |

```json
{"event":"progress","elapsed_ms":1234,"phase":"walking","directories_read":1200,"files_seen":32000,"allocated_bytes":987654321}
```

A full scan's last `walking` record matches the report's `metrics.files_seen`, then come `summarizing` and, unless `--no-save`, `saving`. An incremental rescan reports `loading_cache`, `replaying_changes`, `walking`, `summarizing` and `saving`, and its walking totals count only the folders it read again.

## Versioning

`schema_version` goes up only when a change breaks the contract. New fields can appear at any time without a new version, so ignore fields you don't know. Version 5:

- calls the scan command `scan` (it was `scan.start`);
- adds `exit_code` to every envelope;
- names the scan's unreadable-path summary `read_errors`, leaving `errors` for command failures;
- returns `top --files --json` results as `{files, filters}`.
