JSON Recipes
Contents
- Create
- Read / query
- Modify
- Convert (json ↔ jsonl, json → CSV)
- Validate
- Gotchas
Create
New file with non-ASCII content kept readable:
import json
data = {"name": "Café Müller", "items": [1, 2, 3]}
with open("out.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n") # trailing newline: POSIX-friendly diffsCompact machine-to-machine output:
json.dumps(data, separators=(",", ":"), ensure_ascii=False)Read / query
Safe nested access:
value = data.get("config", {}).get("db", {}).get("host") # None if any level missingFilter a list of records:
active = [r for r in data["users"] if r.get("active")]jq equivalents for large files (jq must be installed):
jq '.users[] | select(.active) | .email' big.json # filter + project
jq 'length' big.json # count
jq -r '.items[].id' big.json # raw strings, no quotesDetect duplicate keys while parsing:
def no_dupes(pairs):
keys = [k for k, _ in pairs]
dupes = {k for k in keys if keys.count(k) > 1}
if dupes:
raise ValueError(f"duplicate keys: {sorted(dupes)}")
return dict(pairs)
data = json.load(open("in.json", encoding="utf-8"), object_pairs_hook=no_dupes)Modify
Atomic in-place update (the only sanctioned write pattern for existing files):
import json, os, tempfile
def update_json(path, mutate):
with open(path, encoding="utf-8") as f:
data = json.load(f)
mutate(data)
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(path)))
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
os.replace(tmp, path) # atomic on POSIX
update_json("config.json", lambda d: d.setdefault("features", {}).update(dark_mode=True))Deep merge (dict-over-dict, right side wins):
def deep_merge(base, override):
out = dict(base)
for k, v in override.items():
out[k] = deep_merge(out[k], v) if isinstance(out.get(k), dict) and isinstance(v, dict) else v
return outShallow diff of two objects:
def diff(a, b):
keys = a.keys() | b.keys()
return {k: (a.get(k), b.get(k)) for k in keys if a.get(k) != b.get(k)}Convert
jsonl → json array:
records = [json.loads(l) for l in open("in.jsonl", encoding="utf-8") if l.strip()]
json.dump(records, open("out.json", "w", encoding="utf-8"), indent=2, ensure_ascii=False)json array → jsonl (streams better, appends safely):
with open("out.jsonl", "w", encoding="utf-8") as f:
for r in json.load(open("in.json", encoding="utf-8")):
f.write(json.dumps(r, ensure_ascii=False) + "\n")Flat records → CSV (nested values must be flattened or stringified first):
import csv
rows = json.load(open("in.json", encoding="utf-8"))
fields = sorted({k for r in rows for k in r})
with open("out.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader(); w.writerows(rows)Flatten nested keys with dots:
def flatten(d, prefix=""):
out = {}
for k, v in d.items():
key = f"{prefix}{k}"
out.update(flatten(v, key + ".")) if isinstance(v, dict) else out.setdefault(key, v)
return outValidate
Syntax: python3 -m json.tool file.json > /dev/null — prints the error with line/column, exit 1 on failure.
Schema:
# pip install jsonschema
from jsonschema import validate, ValidationError
try:
validate(instance=data, schema=schema)
except ValidationError as e:
print(f"invalid at {list(e.absolute_path)}: {e.message}")Gotchas
- Trailing commas, single quotes, comments are the top three causes of
JSONDecodeError— they are JavaScript habits, not JSON. Fix the exact character the error points at. NaN,Infinity:json.dumpsemits them by default but no strict parser accepts them. Useallow_nan=Falseto force the error at write time, then substitutenull.- Encoding: JSON files are UTF-8 by spec; always pass
encoding="utf-8"— Windows defaults to cp1252 and corrupts round-trips. - Float precision:
json.loadgives you binary floats (0.1 + 0.2 != 0.3); for money, parse withjson.load(f, parse_float=decimal.Decimal). - Large ints round-trip fine in Python but break JavaScript beyond 2^53 − 1; stringify IDs above that when the consumer is JS.
sort_keys=Truerewrites the whole file's order — a huge diff for a one-key change. Leave order alone unless asked.- BOM: files from Windows tools may start with U+FEFF; open with
encoding="utf-8-sig"ifjson.loadfails on character 0.