Batu Lab NotesPractical developer guides

Write Deterministic JSON for Local CLI Reports

By Batu · English technical notes

Also published in our Blogger archive.

Batu Lab Notes · Batu

Unstable serialization can create noisy diffs and brittle golden-output comparisons. Define a report policy for indentation, object-key ordering, character encoding, numeric values, array order, and trailing newlines.

Quick answer

For repeatable report files, use fixed indentation, sort object keys, preserve a defined array order, write UTF-8 with LF line endings, and reject non-finite numbers. Check bytes when exact output formatting is part of your CLI contract.

Runnable example

Save this as write_report.py in a temporary working directory. It creates or overwrites report.json in that directory. The filenames inside the report are synthetic strings; the script does not read those files. Run:

python3 write_report.py
import json
from pathlib import Path


def write_report(path: Path, report: dict) -> None:
    rendered = json.dumps(
        report,
        indent=2,
        sort_keys=True,
        ensure_ascii=False,
        allow_nan=False,
    )

    with path.open("w", encoding="utf-8", newline="\n") as output:
        output.write(rendered)
        output.write("\n")


report = {
    "summary": {"warnings": 1, "files_checked": 3},
    "tool": "local-report",
    "findings": [
        {"path": "README.md", "status": "ok"},
        {"path": ".env", "status": "warning"},
    ],
}

write_report(Path("report.json"), report)
print("Wrote report.json")

Expected output

The command prints:

Wrote report.json

The resulting report.json is:

{
  "findings": [
    {
      "path": "README.md",
      "status": "ok"
    },
    {
      "path": ".env",
      "status": "warning"
    }
  ],
  "summary": {
    "files_checked": 3,
    "warnings": 1
  },
  "tool": "local-report"
}

The file ends with one newline after the closing brace.

Serialization policy

Python documents these json options and their behavior in the standard-library reference. [1]

Golden-output fixture

If formatting is part of the CLI contract, compare complete bytes. Create tests/fixtures/report.expected.json, copy the expected JSON shown above, and save it as UTF-8 with LF line endings and one final newline. Then run this check from the working directory:

from pathlib import Path

actual = Path("report.json").read_bytes()
expected = Path("tests/fixtures/report.expected.json").read_bytes()

assert actual == expected

This comparison checks the complete encoded result, including key order, indentation, line endings, and the trailing newline. A text reader may normalize line endings; a byte comparison keeps that difference visible. Review a fixture update against the intended output rather than replacing it automatically when a test fails.

Limits

Related guide

Use the bounded Python text-file comparison guide when you need a readable diff after a byte comparison fails. Avoid sharing a report diff before checking it for private data.

Sources

[1] Python Software Foundation, “json — JSON encoder and decoder,” verified source packet retrieved 2026-09-08. The reference documents indent, sort_keys, ensure_ascii, allow_nan, separators, output ordering, and JSON serialization behavior.

[2] Python documentation: Path.open and file access.

[3] Python documentation: open encoding, newline handling and write mode.

Disclosure: Prepared with AI assistance. The example and expected output were independently checked with synthetic data on Python 3.14.7 on 8 September 2026. This validates the stated cases, not every possible input or operating system.