Write Deterministic JSON for Local CLI Reports
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
indent=2fixes the pretty-printed layout.sort_keys=Truesorts dictionary keys, reducing differences caused only by construction order.ensure_ascii=Falsepreserves non-ASCII characters when the file is written as UTF-8.allow_nan=FalseraisesValueErrorforNaN,Infinity, and-Infinity, which are outside strict JSON.newline="\n"and the explicit final"\n"establish the intended line-ending and trailing-newline 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
- Sorting dictionary keys does not sort arrays. Define array order in the report schema, and sort records only when their order has no semantic meaning.
- JSON consumers should not depend on object-member order for data semantics, even when stable ordering is useful for readable diffs.
- Use consistent string keys when relying on sorted output.
- Exact-text fixtures will change when the serialization policy changes; review such changes intentionally.
allow_nan=Falserejects non-finite floating-point values but does not validate the broader meaning or quality of the report.- This is a report-formatting policy, not a cross-language canonical JSON format for cryptographic signatures. Keep the schema, serializer settings and runtime under review.
- Writing is not atomic and the full report is built in memory. A failed write can leave a partial file. Large reports or crash-safe persistence need a different storage design.
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.
Comments
Post a Comment