Tools
How I Built a JSON Repair Library for LLM Streaming (and Made it 1.7x Faster)
2025-12-31
0 views
admin
The Problem ## The Solution ## Architecture ## What It Fixes ## Advanced Features ## Incremental Mode ## LLM Garbage Extraction ## Web Streams API ## Performance When you stream responses from OpenAI or Anthropic, JSON often arrives incomplete: JSON.parse() throws. Your app crashes. Users see errors. I ran into this problem repeatedly while building AI features, so I wrote a library to fix it: repair-json-stream. LLM APIs stream tokens one at a time. If you're expecting JSON, you have two choices: Existing solutions like jsonrepair work, but they're designed for batch processing, not streaming chunks. I built a single-pass state machine that: The parser uses a stack-based state machine with O(n) single-pass processing: For real-time UI updates, use the stateful incremental repairer: Strip prose and extract JSON from messy LLM outputs: Works natively with TransformStream for edge runtimes: Benchmarked against jsonrepair on Node.js 22: The streaming speedup comes from avoiding repeated full-document parsing. Feedback and contributions welcome! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
{"message": "I'm currently generating your resp Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
{"message": "I'm currently generating your resp CODE_BLOCK:
{"message": "I'm currently generating your resp CODE_BLOCK:
import { repairJson } from 'repair-json-stream' const broken = '{"users": [{"name": "Alice'
const fixed = repairJson(broken)
// → '{"users": [{"name": "Alice"}]}' JSON.parse(fixed) // Works! Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
import { repairJson } from 'repair-json-stream' const broken = '{"users": [{"name": "Alice'
const fixed = repairJson(broken)
// → '{"users": [{"name": "Alice"}]}' JSON.parse(fixed) // Works! CODE_BLOCK:
import { repairJson } from 'repair-json-stream' const broken = '{"users": [{"name": "Alice'
const fixed = repairJson(broken)
// → '{"users": [{"name": "Alice"}]}' JSON.parse(fixed) // Works! CODE_BLOCK:
Input → Preprocessor → State Machine → Output ↓ ↓ Strip wrappers Track: inString, (JSONP, markdown) escaped, stack depth Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
Input → Preprocessor → State Machine → Output ↓ ↓ Strip wrappers Track: inString, (JSONP, markdown) escaped, stack depth CODE_BLOCK:
Input → Preprocessor → State Machine → Output ↓ ↓ Strip wrappers Track: inString, (JSONP, markdown) escaped, stack depth CODE_BLOCK:
import { IncrementalJsonRepair } from 'repair-json-stream/incremental' const repairer = new IncrementalJsonRepair() // As chunks arrive from LLM...
let output = ''
for await (const chunk of llmStream) { output += repairer.push(chunk) updateUI(output) // Live update!
}
output += repairer.end() Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
import { IncrementalJsonRepair } from 'repair-json-stream/incremental' const repairer = new IncrementalJsonRepair() // As chunks arrive from LLM...
let output = ''
for await (const chunk of llmStream) { output += repairer.push(chunk) updateUI(output) // Live update!
}
output += repairer.end() CODE_BLOCK:
import { IncrementalJsonRepair } from 'repair-json-stream/incremental' const repairer = new IncrementalJsonRepair() // As chunks arrive from LLM...
let output = ''
for await (const chunk of llmStream) { output += repairer.push(chunk) updateUI(output) // Live update!
}
output += repairer.end() CODE_BLOCK:
import { extractJson } from 'repair-json-stream/extract' const messy = 'Sure! Here is the data: {"name": "John"} Hope this helps!'
const clean = extractJson(messy)
// → '{"name": "John"}' Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
import { extractJson } from 'repair-json-stream/extract' const messy = 'Sure! Here is the data: {"name": "John"} Hope this helps!'
const clean = extractJson(messy)
// → '{"name": "John"}' CODE_BLOCK:
import { extractJson } from 'repair-json-stream/extract' const messy = 'Sure! Here is the data: {"name": "John"} Hope this helps!'
const clean = extractJson(messy)
// → '{"name": "John"}' CODE_BLOCK:
import { jsonRepairStream } from 'repair-json-stream/web-stream' const response = await fetch('/api/llm')
const repaired = response.body .pipeThrough(new TextDecoderStream()) .pipeThrough(jsonRepairStream()) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
import { jsonRepairStream } from 'repair-json-stream/web-stream' const response = await fetch('/api/llm')
const repaired = response.body .pipeThrough(new TextDecoderStream()) .pipeThrough(jsonRepairStream()) CODE_BLOCK:
import { jsonRepairStream } from 'repair-json-stream/web-stream' const response = await fetch('/api/llm')
const repaired = response.body .pipeThrough(new TextDecoderStream()) .pipeThrough(jsonRepairStream()) - Wait until the entire response is complete (slow, defeats the purpose of streaming)
- Parse incrementally and handle broken JSON (hard) - Repairs truncated strings and unclosed brackets
- Completes partial literals (tru → true, fals → false, nul → null)
- Handles Python-style constants (None, True, False)
- Strips LLM "chatter" like "Here's your JSON:" and thinking blocks
- Works with Web Streams API (Deno, Bun, Cloudflare Workers) - No regex - Avoids ReDoS vulnerabilities
- Character classification bitmask - O(1) lookups for whitespace, quotes, digits
- Minimal allocations - Reuses buffers where possible - Zero dependencies
- 7KB minified
- 97 tests (including property-based testing with fast-check)
- TypeScript-first with full type definitions
- Works in Node.js, Deno, Bun, browsers, Cloudflare Workers - GitHub: https://github.com/prxtenses/repair-json-stream
- npm: https://www.npmjs.com/package/repair-json-stream
- JSR: https://jsr.io/@prxtenses/repair-json-stream
how-totutorialguidedev.toaiopenaillmnodepythongitgithub