# jsonshim

**Get the JSON out of a model's answer, or find out you can't.**

You asked for JSON. You got a code fence, an apology, a trailing comma, a single
quote, and a reply that stopped mid-object because the token budget ran out.
`json.loads` tells you `Expecting value: line 1 column 1 (char 0)` and nothing else.

jsonshim finds the JSON-shaped span, repairs the shapes that are genuinely
repairable, and reports exactly what it changed. When it cannot recover a value it
returns an error and no value. It never fills in a field to avoid an exception —
a confidently wrong object is worse than a stack trace, because the wrong object
gets written to a database.

One file, 18 KB. Standard library only. No install, no network, no telemetry,
no dependency added to your tree. Public domain (CC0).

```
python3 jsonshim.py --selftest        # 37/37, every case listed in the file
cat reply.txt | python3 jsonshim.py   # JSON on stdout, report on stderr, exit 1 if unrecoverable
```

```python
from jsonshim import extract

r = extract(model_reply)
if r.ok:
    use(r.value)          # r.repairs: what was changed. r.span: where it was found.
else:
    retry(r.error)        # no value invented, ever
```

## Install (optional)

Copying the single file into your repo is still the zero-dependency path and always will be.
These two commands are for people who would rather have it in an environment. They install
straight from HTTPS, so neither one needs a package-registry account, a login or a token.

```
npm i https://toolkitlabs.org/pkg/jsonshim-1.0.0.tgz
pip install https://toolkitlabs.org/pkg/toolkitlabs_jsonshim-1.0.0-py3-none-any.whl
```

The npm package installs the JavaScript scorer (`jsonshim-score-js --selftest` prints `15/15`).
The wheel installs the Python module (`from jsonshim import extract`). Source distribution:
https://toolkitlabs.org/pkg/toolkitlabs_jsonshim-1.0.0.tar.gz

Integrity — sha256 of each artefact, and the machine-readable list they come from
(<https://toolkitlabs.org/pkg/index.json>):

```
04ee14e4809a879f18df4d4ca05cb26667c16ac15688dca19b3bd4de9afc635d  jsonshim-1.0.0.tgz   (5611 bytes)
64c9abfd880295c9ca44d13e53e0ab43f9a5b28c6fc035cc483cb63808eb0ca2  toolkitlabs_jsonshim-1.0.0-py3-none-any.whl   (28568 bytes)
f096338d599c26d297b459cb7fd71a9732a1b8e50b533c0136646a6cbb644b07  toolkitlabs_jsonshim-1.0.0.tar.gz   (22549 bytes)
```

Two more paths opened on 2026-08-20, and both were run to exit 0 that day before this line was
written — the JavaScript package is listed on npm, and the Python package installs straight from the
public repository:

```
npm i jsonshim
pip install "git+https://github.com/toolkitlabs/jsonshim#subdirectory=python"
```

Source, and release v1.0.0 with the same wheel and sdist attached (identical sha256 to the list above):
<https://github.com/toolkitlabs/jsonshim>

There is still no PyPI listing — `pip install toolkitlabs-jsonshim` returns 404, checked 2026-08-20,
and is not a command that works.

## What it does

````
$ printf '%s' 'Sure! Here is the call:
```json
{"tool": "search", "args": {"q": "a, b",}, "note": "cut off here' | python3 jsonshim.py --pretty

jsonshim: recovered from span 32..96; repairs: trailing comma removed, truncated input: closed 1 container, unterminated string closed
{
  "tool": "search",
  "args": {
    "q": "a, b"
  },
  "note": "cut off here"
}
````

Prose before and after · fenced blocks, including a fence that never closes ·
trailing commas · single-quoted strings · unquoted keys · `True` / `False` /
`None` / `NaN` / `undefined` · `//` and `/* */` comments · raw newlines inside
strings · mismatched brackets · empty array elements · and truncation, which is
the one that actually matters in production: a reply cut off mid-string,
mid-number, mid-key or four containers deep is closed back up and the incomplete
tail is dropped rather than guessed.

Braces inside strings do not fool the span finder. `{"a": "}"}` parses.

## The numbers

The 33 cases inside `jsonshim.py` were used while writing the repairs, so anything
measured on them is in-sample and worth nothing as a claim. They are published
anyway, and `--selftest` runs them, because a test you can read is better than a
percentage you can't.

The benchmark below is different. Those 30 cases were written after the tool was
finished, run exactly once, and never tuned on. Recovery means the returned value
*equals* the value a human would have written down — not that something parsed.

```
$ python3 bench.py
held-out cases: 30   (written after the tool, run once, never tuned on)
  json.loads baseline exact-match :  9/30  (30.0%)
  jsonshim exact-match            : 28/30  (93.3%)

refuse cases: 3   (any value returned here is a failure)
  jsonshim invented a value       : 0

held-out failures, listed because hiding them would make the number a lie:
  array of tool calls, truncated         -> [{'tool': 'a'}, {'tool': 'b'}, {}]
  colon inside an unquoted-looking url value -> {}
```

Those two failures stay unfixed. Repairing them now would mean tuning on the
held-out set, and 93.3% would stop being a first-run number and start being a
sales figure. They are the honest edges: a truncated object that had a key but no
value becomes `{}` instead of being dropped, and a bare `http://a.b` used as a
value loses to the colon. Neither invents data; both are visible in `r.value`.

**What this benchmark cannot tell you** is how jsonshim does on *your* traffic.
The corpus was written by the same author as the tool. It shows that 30 named
shapes are handled and that the standard library handles 9 of them. Run
`bench.py` with your own failures pasted in — that is the number worth having.

## Design rules

1. Never invent a value. A missing field stays missing.
2. Report every repair. Silent correction is how bad data gets trusted.
3. Prefer the truncation repair, because that is the failure mode that scales
   with your output length and your bill.
4. No dependencies. A JSON repair library that pulls in a parser generator has
   made your problem worse.
5. `want="object"` / `want="array"` when you know the shape and the reply
   contains both.

## Licence

CC0 1.0 Universal. Public domain. No attribution required, no warranty given.
Copy the file into your repo and delete this README.

Tips are optional and buy nothing — there is no paid tier, no key, no account,
and nothing is withheld.
