# Weekend Build Log: Freeze Four JSON Fields First

> Source: <https://dev.to/hackgo_6978/weekend-build-log-freeze-four-json-fields-first-56mo>
> Published: 2026-09-16 03:35:31+00:00

You open the laptop at 9:14 on Saturday. A messy folder of receipt photos sits nearby. You want one command that prints four JSON fields.

The chat pane already wants a dashboard. It also wants OAuth, retries, and a queue. That is how quiet weekends disappear.

This walkthrough stays small on purpose. You freeze a JSON contract before any model runs. Then you ship one command and one validator.

Treat the steps below as a proposed weekend plan. Nothing here is a production benchmark. Your git history is the only scoreboard.

You have maybe six focused hours. You do not have a product team. Scope is the only lever you still control.

A coding helper fills empty hours with new surfaces. Extra surfaces demand extra tests. Extra tests steal the Sunday demo.

So you lock the output shape first. The schema is the product this weekend. Everything else waits in a reject file.

Recent AI coding talk mixes speed with engineering. Speed without a freeze is just churn. A frozen contract is the missing gate.

You will ship three files and one command. That is the whole demo surface.

You will not ship a UI. You will not ship an agent loop. You will not stream tokens into business logic.

Create `contract.json` before you open any helper. Keep four fields only. Name them like an API, not like chat.

```
{
  "version": "1",
  "fields": {
    "merchant": {"type": "string", "max": 80},
    "total_cents": {"type": "integer", "min": 0},
    "currency": {"type": "string", "enum": ["USD", "EUR", "CNY"]},
    "purchased_on": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}
  }
}
```

Print that file. Read each field name out loud. If a field feels fuzzy, delete it now.

Do not add `tax`, `tips`, or `line_items` today. Those fields invite parsers you will not finish. Fuzzy money fields also hide rounding bugs.

Commit the contract before any parser exists. The commit message can be one line. `freeze: four-field receipt contract v1` is enough.

Create `fixtures/receipt_001.json` next. This is the only legal demo input. A second fixture is a later product.

```
{
  "raw_text": "ORBIT CAFE\n2026-09-14\nLatte 4.50\nTotal $4.50",
  "expected": {
    "merchant": "ORBIT CAFE",
    "total_cents": 450,
    "currency": "USD",
    "purchased_on": "2026-09-14"
  }
}
```

One fixture proves the contract path. It does not prove OCR. It does not prove every cafe in town.

If the helper asks for a corpus, refuse. You are proving a shape, not a dataset. Extra photos belong in the reject file.

Redact any real merchant names before you paste. Fake the cafe. Fake the total. Keep private receipts off the prompt.

Create `validate.py` and keep it short. No framework. No JSON Schema plugin this weekend.

``` python
#!/usr/bin/env python3
import json
import sys
from pathlib import Path

CONTRACT = json.loads(Path("contract.json").read_text())
FIELDS = CONTRACT["fields"]

def fail(msg: str) -> None:
    print(f"CONTRACT FAIL: {msg}", file=sys.stderr)
    raise SystemExit(1)

def check(obj: dict) -> None:
    if set(obj) != set(FIELDS):
        fail(f"keys {sorted(obj)} != {sorted(FIELDS)}")
    merchant = obj["merchant"]
    if not isinstance(merchant, str) or not merchant or len(merchant) > 80:
        fail("merchant")
    total = obj["total_cents"]
    if not isinstance(total, int) or isinstance(total, bool) or total < 0:
        fail("total_cents")
    if obj["currency"] not in FIELDS["currency"]["enum"]:
        fail("currency")
    date = obj["purchased_on"]
    if not isinstance(date, str) or len(date) != 10:
        fail("purchased_on")
    if date[4] != "-" or date[7] != "-":
        fail("purchased_on")
    y, m, d = date.split("-")
    if not (y.isdigit() and m.isdigit() and d.isdigit()):
        fail("purchased_on")

def main() -> None:
    payload = json.loads(sys.stdin.read())
    check(payload)
    print("CONTRACT OK")

if __name__ == "__main__":
    main()
```

Run it against the fixture expected block first. You want a green path now. The model has not written anything yet.

``` python
python3 -c 'import json,pathlib; p=json.loads(pathlib.Path("fixtures/receipt_001.json").read_text()); print(json.dumps(p["expected"]))' | python3 validate.py
```

You should see `CONTRACT OK` on stdout. If you do not, stop immediately. Fix the contract by hand, not the helper.

Add a second command that must fail. Feed it a renamed key on purpose. The gate is real only when it rejects drift.

```
echo '{"merchant":"X","amount":1,"currency":"USD","purchased_on":"2026-09-14"}' | python3 validate.py; echo exit:$?
```

That run should print `CONTRACT FAIL` and a non-zero status. Save both commands in `README.md`. Those two lines are the demo script.

Create `parse.py` after the validator is green. Hard-code the fixture path. Do not walk a photo directory.

``` python
#!/usr/bin/env python3
import json
import re
from pathlib import Path

fx = json.loads(Path("fixtures/receipt_001.json").read_text())
text = fx["raw_text"]
merchant = text.splitlines()[0].strip()
date = re.search(r"(\d{4}-\d{2}-\d{2})", text).group(1)
cents = int(round(float(re.search(r"Total \$([0-9.]+)", text).group(1)) * 100))
out = {
    "merchant": merchant,
    "total_cents": cents,
    "currency": "USD",
    "purchased_on": date,
}
print(json.dumps(out))
```

This stub is allowed to look ugly. Ugly and finished beats elegant and open. You can rewrite it next month.

Pipe the stub into the validator.

```
python3 parse.py | python3 validate.py
```

Green output is the weekend demo. Record that pipe in `README.md`. Do not open a browser to prove it.

If the regex fails, edit `parse.py` yourself. Do not start a second helper session yet. Hands-on edits keep the schema honest.

Create `REJECT.md` beside the contract. Write refusals as concrete items. Vague principles will not stop a chatty model.

```
# Reject this weekend
- web dashboard
- auth and sessions
- PDF OCR pipeline
- second fixture
- retry loop around the model
- streaming UI
- database
- extra money fields
```

Read that list before every helper prompt. If the reply adds a rejected surface, discard the reply. Do not negotiate with the extra files.

The reject file is a build tool. It is not a confession. You are buying a Sunday demo with Saturday restraint.

If you use a coding helper, paste `contract.json` first. Paste `REJECT.md` second. Ask only for a four-field `parse.py`.

Do not ask it to make this production ready. That phrase deletes weekends. Ask for stdout JSON that matches the contract.

After the first patch, stop the helper. Run the validator again. If it fails, you edit the stub by hand.

This is a gate, not a leaderboard. It does not measure model quality. It measures whether your weekend still has a shape.

Use this table when the helper gets generous. Print it. Keep it above the keyboard.

| Incoming request | Saturday action | 
|---|---|
| Add a fifth JSON field | Refuse; append it to `REJECT.md` | 
| Rename `total_cents` | Refuse; keep the frozen key | 
| Stand up a React app | Refuse | 
| Add a second fixture | Refuse until the pipe stays green | 
| Validator false positive | Fix `validate.py` by hand | 
| Parser misses the date | Edit `parse.py` ; no new loop | 
| "Make it robust" | Refuse | 
| Public URL with open prompt | Refuse | 

The table is the original artifact. Code can drift. The table states the policy in one glance.

You may want a model for the first `parse.py` draft. You may later want a throwaway host for one JSON endpoint. Both come after the contract exists.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode provides free model access and a free server option you can use for that bounded weekend pass. Keep `contract.json` in your repo either way. The helper does not own the shape.

A later endpoint should accept one JSON body. It should return four fields or an error. Reuse `validate.py` as the server gate if you host anything.

Do not point a public URL at an unbounded prompt. A free server is not a license for a dashboard. The reject file still wins.

The parser will choke on missing dollar signs. That is acceptable this weekend. Document the miss in `REJECT.md` and move on.

The helper will rename `total_cents` to `amount`. The validator must reject that rename. Do not stretch the contract to match the model.

Currency enums will feel rude. Keep them rude. Open string fields become soup by Sunday night.

Dates without hyphens will fail the gate. Good. Your demo is a contract, not a guesser. Guessers do not survive the next fixture.

Boolean `true` must not pass as `total_cents`. Python treats `bool` as `int` without the extra check. That is why the validator excludes `bool` explicitly.

This approach ignores real OCR. It ignores multi-line merchant names. It ignores tax-inclusive totals and tips.

It also ignores load, auth, and retention. Those are product problems. They are not Saturday problems.

The validator is not a schema platform. It is a weekend gate. Replace it later if the contract survives a month.

Free model output can drift across sessions. You do not pin a vendor by hoping. You pin a file in git and a failing command.

A free server is not a production posture. Treat it as a demo host only. Put secrets nowhere near that host.

This article does not name models, quotas, or hardware. Those claims go stale fast. Your contract file does not.

Do not use this if Monday needs every receipt parsed. You need a real pipeline then. A stub will lie to finance.

Do not use this if compliance needs an audit trail. A weekend script is not that trail. You will fake safety and regret it.

Do not use this if your team already owns a registry. Follow that registry instead. Do not fork a toy schema beside it.

Do not paste private receipts into any helper. Redact the fixture. Fake the cafe. Keep personal amounts local.

Skip this if you cannot freeze keys. If every chat can rename fields, you do not have a product. You have a conversation.

Your done state is one pipe that prints `CONTRACT OK`. That is enough for Sunday. Anything past that is next weekend.

If the helper still wants a dashboard, reread `REJECT.md`. Then close the chat. Ship the command and walk away.
