Before you begin
- Python 3.12 or newer
Set up your example
- Save the complete code as json-unflatten-paths.py in a working folder.
- Save the provided sample files in that same folder; run commands from that folder.
Sample files in this download:
- types.json
- sample.json
- people.json
- other.json
- sample.jsonl
- flat.json
- sample.xml
Download the ZIP, extract it, and open a terminal inside the extracted folder. The source file is json-unflatten-paths.py.
Run the script
python "json-unflatten-paths.py" --input "flat.json" --output "rebuilt.json"If your system uses python3 instead of python, use that command for Python examples. Keep paths containing spaces in quotes. Required inputs and optional settings are described in the code and setup steps.
What to expect
Rebuild nested JSON from typed path/value records. The fixture restores team Red and a member named Ada.
The complete script
#!/usr/bin/env python3
'Reconstruct JSON from typed paths'
import argparse
import json
from pathlib import Path
def write_json(path, value):
data = json.dumps(value, indent=2, ensure_ascii=False, allow_nan=False) + "\n"
with Path(path).open("x", encoding="utf-8", newline="") as handle:
handle.write(data)
import random
from collections import Counter
import xml.etree.ElementTree as ET
def read_text(path):
if Path(path).stat().st_size > 10*1024*1024:
raise ValueError("Input exceeds 10 MiB.")
return Path(path).read_text(encoding="utf-8-sig")
def reject(value):
raise ValueError("Nonstandard JSON constant: "+value)
def load(path):
return json.loads(read_text(path),parse_constant=reject)
def lines(path):
return [json.loads(s,parse_constant=reject) for s in read_text(path).splitlines() if s.strip()]
def array(value):
if not isinstance(value,list):
raise ValueError("Expected an array root.")
return value
def objects(value):
value=array(value)
if not all(isinstance(r,dict) for r in value):
raise ValueError("Every array item must be an object.")
return value
def walk(value,path=()):
yield path,value
if isinstance(value,dict):
for k,v in value.items(): yield from walk(v,path+(k,))
elif isinstance(value,list):
for i,v in enumerate(value): yield from walk(v,path+(i,))
def pointer(value,path):
if not path: return value
if not path.startswith("/"): raise ValueError("Use a /-prefixed JSON Pointer.")
for part in path[1:].split("/"):
key=part.replace("~1","/").replace("~0","~")
if isinstance(value,list):
if not key.isdigit() or int(key)>=len(value): raise ValueError("Bad array index.")
value=value[int(key)]
elif isinstance(value,dict): value=value[key]
else: raise ValueError("Pointer crosses a scalar.")
return value
def xml(path):
text=read_text(path)
if "<!DOCTYPE" in text.upper() or "<!ENTITY" in text.upper():
raise ValueError("DTD/entity declarations are refused.")
try: return ET.fromstring(text)
except ET.ParseError as exc: raise ValueError(str(exc)) from exc
def save(path,text):
with Path(path).open("x",encoding="utf-8",newline="") as handle: handle.write(text)
def main():
parser = argparse.ArgumentParser(description='Reconstruct JSON from typed paths')
parser.add_argument("--input", required=True)
parser.add_argument("--output", required=True)
args = parser.parse_args()
records=objects(load(args.input))
paths=[]
for row in records:
path=row.get("path")
if not isinstance(path,list) or "value" not in row or any(type(k) not in (str,int) or (type(k)==int and k<0) for k in path): raise ValueError("Need typed path arrays and values.")
key=tuple(path)
if any(key==p[:len(key)] or p==key[:len(p)] for p in paths): raise ValueError("Duplicate or overlapping paths.")
paths.append(key)
def rebuild(group,depth=0):
direct=[r["value"] for r in group if len(r["path"])==depth]
if direct: return direct[0]
if not group: raise ValueError("No root value.")
keys={r["path"][depth] for r in group}
if all(type(k)==str for k in keys): return {k:rebuild([r for r in group if r["path"][depth]==k],depth+1) for k in sorted(keys)}
if all(type(k)==int for k in keys) and keys==set(range(len(keys))): return [rebuild([r for r in group if r["path"][depth]==k],depth+1) for k in range(len(keys))]
raise ValueError("Mixed key types or missing array positions.")
write_json(args.output,rebuild(records))
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, KeyError, TypeError, ArithmeticError, IndexError, LookupError) as exc:
raise SystemExit(str(exc))
Open source as plain text · Download source and sample files
How it works
- Local UTF-8 input is limited to 10 MiB and held in memory.
- The rebuild function rejects overlapping paths, mixed key types, and array gaps rather than guessing missing structure.
- The fixture restores team Red and a member named Ada.
Make it your own
--input
Use the output of json-flatten-paths for a structural round trip.
Adapt the processing
Edit rebuild's diagnostics to include the current depth.
Change one setting at a time, run it again on sample data, and compare the output. The customization guide walks through this process.
If something goes wrong
An array gap is rejected.
Supply every zero-based array position; missing indices are not filled automatically.
The destination already exists.
Choose a new --output path. The script deliberately refuses to replace an existing result.
Reference & next steps
These official references explain the underlying tools. The Scriptage example and guide are original instructional content. See verification notes for what has been checked and the limits of those checks.
Copies a template for your own notes. It does not send anything.