DedSec Project

A safe JSON notes helper — DedSec Project

Recipe: validated JSON notes with failure-safe replacement Scenario and acceptance criteria Store a tiny offline checklist containing no more than ten notes. Each entry must have exactly…

Recipe: validated JSON notes with failure-safe replacement

Scenario and acceptance criteria

Store a tiny offline checklist containing no more than ten notes. Each entry must have exactly title and done; the title must be between one and 80 code points and the completion flag must be a real Boolean. Serialize before writing, use a temporary file in the destination directory and replace the old file only when validation and writing succeed. The invalid-entry test must leave the previous bytes unchanged.

Complete executable example

Save as 01_Safe_JSON_notes.py and run with Python 3 and the standard library. The fixture works without internet or access to your real files.

import json, os, tempfile
from pathlib import Path
def save_notes(destination, notes):
    if not isinstance(notes, list) or len(notes) > 10:
        raise ValueError("expected up to ten notes")
    if not all(isinstance(n, dict) and set(n)=={"title","done"}
               and type(n["title"]) is str and 1<=len(n["title"])<=80
               and type(n["done"]) is bool for n in notes):
        raise ValueError("invalid note schema")
    data = json.dumps(notes,ensure_ascii=False,sort_keys=True).encode("utf-8")
    dest = Path(destination)
    name = None
    try:
        with tempfile.NamedTemporaryFile(dir=dest.parent,prefix=".notes-",
                                         delete=False) as temp:
            name = temp.name
            temp.write(data)
            temp.flush()
            os.fsync(temp.fileno())
        os.replace(name,dest)
        name = None
    finally:
        if name is not None:
            Path(name).unlink(missing_ok=True)
with tempfile.TemporaryDirectory() as d:
    target=Path(d)/"notes.json"
    save_notes(target,[{"title":"Ελληνικά","done":False}])
    before=target.read_bytes()
    try: save_notes(target,[{"title":"","done":"no"}])
    except ValueError: pass
    else: raise AssertionError("invalid entry accepted")
    assert target.read_bytes()==before
    assert json.loads(before.decode("utf-8"))[0]["title"]=="Ελληνικά"
print("PASS schema-checked atomic JSON fixture")

Technical explanation

JSON serialization does not validate business rules: a string 'no' is not a Boolean and an empty title violates the chosen model. Creating the entire byte payload before touching the filesystem prevents late JSON conversion errors from corrupting the old report. Flush and fsync improve durability of the temporary file before os.replace. A successful rename is not the same as guaranteed power-loss durability or safe handling of an attacker-controlled directory; production storage may need directory fsync and secure path ownership.

Bounded practice

Add duplicate note identifiers and decide whether the schema should require unique IDs. Simulate an error before os.replace and verify that the previous file survives. Try Greek accents, emoji and unexpected properties. Enforce an encoded byte-size limit as well as a note-count limit before accepting user-controlled strings.

Diagnostics and safety

If a JSON file fails to load, capture JSONDecodeError without deleting or overwriting the original. Do not simply treat every parse failure as an empty checklist: doing so may erase recoverable user data. When an OS write fails, preserve the old destination and report a meaningful error.