O(1) streaming JSON parser for the AI era. Zero dependencies. ~4KB gzipped.
Every other "streaming JSON" library re-parses the entire accumulated string on every chunk — O(n²) total. StreamJSON processes each byte exactly once and maintains a live object tree. parser.get() returns the current state in O(1).
STREAMING (simulated LLM token delivery, 612 bytes)
Library Median (ms) Speedup
StreamJSON 0.03 1.0x ⚡
partial-json 1.56 55x
jsonrepair 2.21 78x
best-effort 0.99 35x
SCALE TEST (per-chunk cost as payload grows)
Size StreamJSON (µs/chunk) partial-json (µs/chunk) Ratio
1KB 0.27 27 100x
10KB 0.21 150 700x
50KB 0.22 690 3,000x+
StreamJSON's per-chunk cost stays flat. Competitors grow linearly.
npm install @a5omic/streamjsonimport { StreamJSON } from '@a5omic/streamjson'
const parser = new StreamJSON()
// Feed chunks as they arrive from your LLM
parser.push('{"na')
parser.push('me": "Jo')
parser.push('hn", "age": 30}')
parser.end()
parser.get() // { name: "John", age: 30 }const parser = new StreamJSON({ emitPartial: true })
parser.push('{"name": "Jo')
parser.get() // { name: "Jo" } — string still being streamed
parser.push('hn", "age": 30}')
parser.end()
parser.get() // { name: "John", age: 30 }const parser = new StreamJSON()
parser.on('value', (path, value, isComplete) => {
console.log(path, value) // ["name"] "John", ["age"] 30
})
parser.on('object_start', (path) => { /* { opened */ })
parser.on('array_end', (path) => { /* ] closed */ })
parser.push('{"name": "John", "age": 30}')
parser.end()// Like JSON.parse but handles trailing commas, truncated input, etc.
StreamJSON.parse('{"a": 1,}') // { a: 1 }
StreamJSON.parse('{"a": "trunc') // { a: "trunc" }const parser = new StreamJSON()
parser.push('{"a": 1}')
parser.end()
parser.reset() // ready for next message
parser.push('[1, 2, 3]')
parser.end()
parser.get() // [1, 2, 3]npm install @a5omic/streamjson-reactimport { useStreamJSON } from '@a5omic/streamjson-react'
function Chat() {
const { push, end, reset, value, isComplete } = useStreamJSON()
// Feed chunks from your streaming API
useEffect(() => {
const stream = fetchLLMStream()
stream.on('data', (chunk) => push(chunk))
stream.on('end', () => end())
return () => reset()
}, [])
return <pre>{JSON.stringify(value, null, 2)}</pre>
}import { StreamJSON } from '@a5omic/streamjson-react'
function ToolCall({ streamingJSON, done }) {
return (
<StreamJSON content={streamingJSON} complete={done}>
{(value, isComplete) =>
value ? <ToolCard tool={value} loading={!isComplete} /> : <Skeleton />
}
</StreamJSON>
)
}StreamJSON handles the specific ways LLMs produce malformed JSON:
| Scenario | Input | Output |
|---|---|---|
| Truncated string | {"name": "Joh |
{ name: "Joh" } |
| Truncated number | {"val": 123 |
{ val: 123 } |
| Truncated keyword | {"ok": tru |
{ ok: true } |
| Unclosed containers | {"a": [1, 2 |
{ a: [1, 2] } |
| Trailing comma | {"a": 1,} |
{ a: 1 } |
| Missing comma | {"a": 1 "b": 2} |
{ a: 1, b: 2 } |
emitPartial?: boolean— Update partial string values in the live object as characters arrive (default:false)
push(chunk: string)— Feed a chunk. O(chunk.length) per call.end()— Signal stream complete. Flushes pending state, closes open containers.get(): unknown— Return current parsed value. O(1).reset()— Clear all state for reuse.on(event, handler)/off(event, handler)— Subscribe to events.
value(path, value, isComplete)— A value was parsed or updatedobject_start(path)/object_end(path)— Object opened/closedarray_start(path)/array_end(path)— Array opened/closederror(error, position)— Parse error encountered (recoverable)
Static convenience. Like JSON.parse but tolerant of truncated/malformed input.
- Prototype pollution immune: All parsed objects use
Object.create(null)— no__proto__,constructor, orprototypechain attacks. - No regex in hot path: Zero ReDoS risk.
- Invalid unicode/escape handling: Emits error events instead of silently producing corrupt values.
StreamJSON is a push-based state machine that processes each byte exactly once. Instead of building an AST, it mutates a live JavaScript object tree directly — the same approach that made Flowdown 2,146x faster than marked for streaming markdown.
Why competitors are slow: partial-json and similar libraries call parse(accumulatedString) on every chunk. At chunk N, they re-parse all N previous chunks. Total work: 1 + 2 + 3 + ... + N = O(N²).
Why StreamJSON is fast: Each push(chunk) processes only the new bytes and updates the object tree in place. Total work: O(N).
MIT