#!/usr/bin/env python3 """ json_to_csv.py — convert a JSON array of flat objects to a CSV file. Features: - Reads a JSON file containing an array of flat objects (or from stdin) - Column order = first appearance across the array - Missing keys are filled with an empty string - None -> empty string; nested values are JSON-encoded as strings - Writes to stdout or an output file - Clear error messages and non-zero exit codes Python 3.10+, stdlib only. Usage: python3 json_to_csv.py input.json python3 json_to_csv.py input.json -o output.csv cat input.json | python3 json_to_csv.py """ import argparse import csv import io import json import sys def load_data(path): if path: with open(path, "r", encoding="utf-8") as fh: text = fh.read() else: text = sys.stdin.read() if not text.strip(): raise ValueError("No input provided (empty file/stdin)") try: data = json.loads(text) except json.JSONDecodeError as exc: raise ValueError(f"Invalid JSON: {exc}") from exc if not isinstance(data, list): raise ValueError("Top-level JSON value must be an array of objects") if not data: raise ValueError("JSON array is empty: nothing to convert") for i, item in enumerate(data): if not isinstance(item, dict): raise ValueError(f"Item {i} is not an object: {type(item).__name__}") return data def cell(value): """Convert a JSON value to a CSV cell string. None -> empty string.""" if value is None: return "" if isinstance(value, str): return value if isinstance(value, (bool, int, float)): return "true" if value is True else ("false" if value is False else str(value)) # dict/list: encode as compact JSON string return json.dumps(value, ensure_ascii=False) def convert(data): """Build rows and column order from the array of flat objects.""" columns = [] seen = set() for item in data: for key in item: if key not in seen: seen.add(key) columns.append(key) rows = [] for item in data: rows.append([cell(item.get(key)) for key in columns]) return columns, rows def main(argv=None): parser = argparse.ArgumentParser(description="Convert a JSON array of flat objects to CSV.") parser.add_argument("input", nargs="?", help="JSON file path (default: read from stdin)") parser.add_argument("-o", "--output", default=None, help="Write CSV to file instead of stdout") args = parser.parse_args(argv) try: data = load_data(args.input) columns, rows = convert(data) buf = io.StringIO() writer = csv.writer(buf, lineterminator="\n") writer.writerow(columns) writer.writerows(rows) out = buf.getvalue() if args.output: with open(args.output, "w", encoding="utf-8", newline="") as fh: fh.write(out) else: sys.stdout.write(out) except ValueError as exc: print(f"error: {exc}", file=sys.stderr) return 2 except OSError as exc: print(f"error: {exc}", file=sys.stderr) return 2 return 0 if __name__ == "__main__": sys.exit(main())