#!/usr/bin/env python3 """Schema-aware JSON -> CSV exporter (Python 3 stdlib only). Deterministic, dependency-free. Produces explicit, auditable CSV from JSON with a stable column order, type-aware flattening, and header control. """ import csv, io, json, sys from collections import OrderedDict def _is_scalar(v): return v is None or isinstance(v, (str, int, float, bool)) def flatten(value, prefix=""): """Return (columns, row) mapping for a single object value, recursive.""" cols = [] row = [] if isinstance(value, dict): for k, v in value.items(): key = f"{prefix}.{k}" if prefix else k if isinstance(v, dict): c, r = flatten(v, key) cols.extend(c); row.extend(r) elif isinstance(v, list): cols.append(key); row.append(json.dumps(v, ensure_ascii=False, separators=(',', ':'))) else: cols.append(key); row.append("" if v is None else v) elif isinstance(value, list): # array of scalars -> semicolon-joined; array of objects -> keep as one cell (JSON) cols.append(prefix or "value") row.append(json.dumps(value, ensure_ascii=False, separators=(',', ':'))) else: cols.append(prefix or "value") row.append("" if value is None else value) return cols, row def schema_aware_json_to_csv(input_path, output_path, header=True, encoding="utf-8"): with open(input_path, "r", encoding=encoding) as f: data = json.load(f) if isinstance(data, dict) and not _is_scalar(data) and all(isinstance(v, dict) for v in data.values()): records = list(data.values()) elif isinstance(data, list): records = data else: records = [data] # Determine stable column order from first record + any extras columns = [] for rec in records: c, _ = flatten(rec) for col in c: if col not in columns: columns.append(col) rows = [] for rec in records: cols, row = flatten(rec) m = dict(zip(cols, row)) rows.append([m.get(col, "") for col in columns]) with open(output_path, "w", newline="", encoding=encoding) as f: w = csv.writer(f) if header: w.writerow(columns) w.writerows(rows) return columns, len(rows) def json_to_csv_from_text(text, header=True): data = json.loads(text) out = io.StringIO() if isinstance(data, list): records = data elif isinstance(data, dict) and all(isinstance(v, dict) for v in data.values()): records = list(data.values()) else: records = [data] columns = [] for rec in records: c, _ = flatten(rec) for col in c: if col not in columns: columns.append(col) w = csv.writer(out) if header: w.writerow(columns) for rec in records: cols, row = flatten(rec) m = dict(zip(cols, row)) w.writerow([m.get(col, "") for col in columns]) return out.getvalue() if __name__ == "__main__": if len(sys.argv) < 3: print("usage: jsontocsv.py ") sys.exit(2) cols, n = schema_aware_json_to_csv(sys.argv[1], sys.argv[2]) print(f"columns={len(cols)} rows={n}")