json-mending
v0.1.1
Published
Repair malformed JSON from LLM outputs: unterminated strings, trailing commas, markdown fences, and more.
Maintainers
Readme
json-mending
Repair malformed JSON from LLM outputs. Zero dependencies. Never throws (unless you ask it to). Every fix it makes is recorded, so you can see exactly what changed.
import { repairJson } from 'json-mending';
repairJson('{"a": 1,}');
// { ok: true, value: { a: 1 }, repairs: [{ type: 'removed-trailing-comma', position: 7 }] }Why
LLMs asked for JSON don't always produce valid JSON. Output gets cut off
mid-token when it hits max_tokens. Chat-tuned models wrap it in
```json fences even when told not to. Models trained on lots of
JavaScript write trailing commas and single-quoted strings without thinking
twice. JSON.parse rejects all of it outright, so most LLM pipelines end up
with a hand-rolled try/catch and a pile of regexes.
json-mending tokenizes and parses the input properly instead of
pattern-matching text, so it can repair structure — missing commas,
unclosed brackets, a truncated value — rather than just cleaning up
surface syntax.
Install
npm install json-mendingUsage
import { repairJson, repairJsonOrThrow } from 'json-mending';
// repairJson never throws — check `ok` instead
const result = repairJson('```json\n{name: \'Ada\', "tags": [1, 2,]\n```');
if (result.ok) {
console.log(result.value); // { name: 'Ada', tags: [1, 2] }
console.log(result.repairs); // every fix that was applied, in order
}
// repairJsonOrThrow throws on truly unparseable input, like JSON.parse
const value = repairJsonOrThrow('{"a": 1,}'); // { a: 1 }Options
repairJson(input, {
// How to handle an object member whose value was cut off entirely,
// e.g. `{"a":1,"b":` at EOF with nothing after the colon.
onTruncation: 'drop-partial', // (default) discard the dangling key
// onTruncation: 'keep-partial', // keep it with a `null` value instead
});What it handles
- Markdown code fences (with or without a
jsonlanguage tag) - Leading prose before the JSON value ("Sure, here's the JSON you asked for: ...")
- Unterminated strings — cut off at end of input, or at a raw unescaped newline (which is never valid inside a JSON string, so anything after it is treated as separate structure, not more string content)
- Trailing, leading, and doubled commas
- Missing commas between object members or array elements
- Unclosed objects/arrays at end of input, including nested ones
- Single-quoted strings and unquoted object keys (
{name: 'Ada'}) - Unescaped quotes embedded inside a string value (
"The TV is 72" wide") — a candidate closing quote is only trusted as the real end if what follows looks like the start of the next token; otherwise it's treated as content - Truncated keywords at end of input (
tru→true) - Leading-dot numbers (
.5→0.5) - Invalid numbers — dropped rather than fabricated, and logged
- Stray tokens anywhere in the middle of a structure — a bad token is skipped, not treated as the end of the array or object
Every one of these is backed by a fixture in
src/__fixtures__/corpus.ts and exercised by
property-based fuzz tests in src/fuzz/ that generate random
JSON, corrupt it, and check the exact original value comes back out.
What it doesn't handle (yet)
- Python-style literals (
True,False,None) — only lowercase JSON keywords are recognized - Comments (
//,/* */) inside the JSON NaN/Infinityas literal tokens
None of these are structural — patches are welcome.
API
repairJson(input, options?) => RepairResult
type RepairResult =
| { ok: true; value: unknown; repairs: RepairLog[] }
| { ok: false; error: string; repairs: RepairLog[] };Valid JSON takes a fast path straight through JSON.parse with zero
tokenizing overhead and an empty repairs array. Only malformed input pays
for the lenient tokenizer/parser.
repairJsonOrThrow(input, options?) => unknown
Same repair behavior as repairJson, but returns the value directly and
throws an Error instead of returning { ok: false }.
RepairLog
A discriminated union describing exactly what was fixed and where (as a
character offset into the input), e.g. { type: 'removed-trailing-comma',
position: 8 }. See src/types.ts for the full list of
repair kinds.
Status
Covers the common structural failure modes seen in real LLM output, not every conceivable malformed input. See the gaps above.
