Mends the JSON your LLM almost wrote.
Truncated tool calls, markdown fences, single quotes, bare keys, Python literals, comments, trailing commas, prose around the payload — jsonmend turns them into valid JSON. It is a drop-in replacement for json_repair that is 5–10× faster on batch repair, ~50× faster on streaming, ships a true incremental streaming API (O(new bytes) per chunk, not O(buffer)), and is the reference implementation of an open, cross-language conformance corpus for JSON repair.
Pure Python, zero dependencies, zero binaries. Works on CPython 3.9–3.14,
PyPy, Pyodide/WASM, AWS Lambda — anywhere pip install works.
pip install jsonmendJSON repair has no standard: the same broken input is repaired differently by the Python and JavaScript incumbents, which is a real source of production bugs. The jsonmend conformance corpus (485 cases, 20 categories, CC0) defines repair semantics as data — including the genuinely ambiguous cases, where every defensible answer is accepted.
| jsonmend 0.1.1 | json_repair 0.60.1 | jsonrepair 3.14.0 (JS) | |
|---|---|---|---|
| corpus pass rate | 485/485 (100%) | 339/485 (69.9%) | 365/485 (75.3%) |
Per-category breakdown: corpus/scoreboard.md.
Reproduce: python tools/referee.py --write (needs pip install json_repair and npm install jsonrepair, dev-only).
Median of 7, three independent rounds within ±5%, Python 3.12, M-series
macOS. All inputs are broken JSON (the json.loads fast path never
runs). Verified-then-timed: outputs are checked equal before timing.
Reproduce: python tools/bench.py --verify && python tools/bench.py.
| workload | size | jsonmend | json_repair | speedup |
|---|---|---|---|---|
| truncated tool call | 1 KB | 0.027 ms | 0.199 ms | 7.3× |
| truncated row payload | 75 KB | 1.48 ms | 12.6 ms | 8.5× |
| markdown-fenced output | 49 KB | 0.25 ms | 2.6 ms | 10.6× |
| dirty (quotes/keys/literals) | 5 KB | 0.38 ms | 2.6 ms | 7.0× |
A streaming UI re-renders the partial value on every chunk. With a batch
repairer you must re-parse the whole buffer each time — O(n²) total. The
stateful Mender only pays for the new bytes (amortised O(1) per fed
character on CPython — guarded by a buffer-copy invariant test, not a
flaky timer):
| workload | jsonmend Mender |
json_repair (stream_stable=True) |
|
|---|---|---|---|
| 150 KB in 4 KB chunks | 6.9 ms | 323 ms | 47× |
| 10 MB in 4 KB chunks | 1.2 s | est. >20 min (quadratic) | — |
# before
from json_repair import repair_json, loads
# after — same call sites
from jsonmend import repair_json, loads
repair_json("{'name': 'John', age: 31") # '{"name": "John", "age": 31}'
loads('```json\n{"ok": true,}\n```') # {'ok': True}repair_json(json_str, return_objects=..., skip_json_loads=..., ensure_ascii=..., **json_dumps_args), loads, load(fd),
from_file(path) match json_repair's signatures. Valid JSON
short-circuits through C-speed json.loads.
from jsonmend import Mender
m = Mender()
for chunk in llm_stream: # feed as the tokens arrive
partial = m.feed(chunk) # best-effort value, O(new bytes)
render(partial) # e.g. {"answer": "The capital of Fr"}
value = m.close() # final mended valuefeed() returns a live view that grows in place — including the string
that is currently streaming in. Any chunking gives byte-identical results
to batch repair (property-tested over the whole corpus).
from jsonmend import loads, JSONMendError
loads("complete garbage") # "" (json_repair-compatible)
loads("complete garbage", strict=True) # raises JSONMendErrortruncated objects/arrays/strings/numbers/literals · markdown fences with
prose around them · single/smart/backtick quotes · unescaped inner quotes
· missing quotes · bare keys and values · True/False/None/undefined/NaN/ Infinity · //, #, /* */ comments · trailing/missing/extra commas ·
missing colons · mismatched brackets · concatenated/NDJSON documents ·
string concatenation ("a" + "b") · JSONP/MongoDB wrappers
(ObjectId("…")) · Python tuples/sets · ellipsis placeholders ·
non-string keys · BOM and exotic whitespace · escaped-JSON documents
({\"a\": 1}) · broken \u escapes and surrogate pairs · 100k-deep
nesting (no recursion anywhere)
- One resumable state machine serves batch and streaming — batch is a single feed that never suspends, so there is no streaming tax.
- Strings cost one
str.find+ one slice when clean; never a per-character Python loop. - Speculative C parsing: complete sub-trees inside broken documents
are recognized and handed to the C
jsonscanner, with a salvage step that parses the longest clean prefix of a broken container in one shot. Semantics-affecting inputs (NaN, control chars, surrogate escapes) fall back to the machine, so behavior never changes. - Bounded backtracking: a string-close decision can revisit one recorded candidate quote, never rescan; adversarial quote storms stay linear (tested).
- Output is always valid RFC 8259 JSON (or
""/an exception). Unlike json_repair,NaN/Infinitynever leak into the output text — they serialize asnull(loadsstill gives you the floats). - Output is always UTF-8 encodable (lone surrogates are replaced).
- Never crashes, never recurses: fuzzed and property-tested, 100k-deep inputs are fine.
Mender.close()≡ batch result, for every chunking (property-tested).
logging=Trueis not supported (it is incompatible with the single-pass design and is one reason json_repair is slow); a no-op shim raisesTypeErrorso you notice.- Schema-guided repair (
schema=) is not implemented in v0.1. - json_repair's
stream_stable=Trueflag changes how truncated escapes render mid-stream; jsonmend'sMenderis always stream-stable. - On
ambiguouscorpus cases the libraries may legitimately differ; jsonmend's choices are documented case-by-case in the corpus rationales.
If you maintain a JSON-repair library in any language: please steal corpus/. It is CC0, the format is three fields, and 485 cases with rationales are more valuable than any of our engines. Cross-language agreement on repair semantics helps everyone shipping LLM systems.
MIT. The conformance corpus is CC0.