typescript
64 lines · 9 steps
Streaming JSON parsing with a depth counter
A ReadableStream transform that emits complete JSON values as they arrive, tracking brace depth to find object boundaries.
Explained by
highlit
1type JsonValue = Record<string, unknown> | unknown[];
2
3export function parseJsonStream<T = JsonValue>(
4 stream: ReadableStream<Uint8Array>,
5): ReadableStream<T> {
6 const decoder = new TextDecoder();
7 let buffer = "";
8
9 return new ReadableStream<T>({
10 async start(controller) {
11 const reader = stream.getReader();
12 try {
13 while (true) {
14 const { done, value } = await reader.read();
15 if (done) break;
16 buffer += decoder.decode(value, { stream: true });
17 buffer = drain(buffer, controller);
18 }
19 buffer += decoder.decode();
20 drain(buffer, controller, true);
21 controller.close();
22 } catch (err) {
23 controller.error(err);
24 } finally {
25 reader.releaseLock();
26 }
27 },
28 });
29
30 function drain(input: string, controller: ReadableStreamDefaultController<T>, final = false): string {
31 let depth = 0;
32 let inString = false;
33 let escaped = false;
34 let start = -1;
35
36 for (let i = 0; i < input.length; i++) {
37 const ch = input[i];
38 if (inString) {
39 if (escaped) escaped = false;
40 else if (ch === "\\") escaped = true;
41 else if (ch === '"') inString = false;
42 continue;
43 }
44 if (ch === '"') inString = true;
45 else if (ch === "{" || ch === "[") {
46 if (depth === 0) start = i;
47 depth++;
48 } else if (ch === "}" || ch === "]") {
49 depth--;
50 if (depth === 0 && start !== -1) {
51 controller.enqueue(JSON.parse(input.slice(start, i + 1)) as T);
52 input = input.slice(i + 1);
53 i = -1;
54 start = -1;
55 }
56 }
57 }
58
59 if (final && input.trim().length > 0) {
60 throw new SyntaxError("Trailing incomplete JSON in stream");
61 }
62 return input;
63 }
64}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Tracking nesting depth lets you split a byte stream into complete values without buffering everything.
- 2String and escape state must be tracked separately so braces inside quoted strings don't corrupt the count.
- 3Passing { stream: true } to TextDecoder handles multi-byte characters split across chunk boundaries.
Related explainers
ruby
class Order class InvalidTransition < StandardError; end TRANSITIONS = {
A state machine for order transitions in Ruby
state-machine
data-driven
error-handling
Intermediate
8 steps
typescript
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config';
How a JWT strategy authenticates in NestJS
authentication
jwt
passport
Intermediate
7 steps
python
import time import threading from enum import Enum from functools import wraps
Building a circuit breaker in Python
circuit-breaker
resilience
decorators
Advanced
7 steps
typescript
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { catchError, filter, take, switchMap } from 'rxjs/operators';
Refreshing auth tokens in an Angular interceptor
http-interceptor
token-refresh
rxjs
Advanced
8 steps
rust
use std::cmp::Ordering; use std::str::FromStr; #[derive(Debug, Clone, PartialEq, Eq)]
Parsing and ordering semantic versions in Rust
parsing
trait-implementation
ordering
Intermediate
8 steps
typescript
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt';
How a GraphQL auth guard works in NestJS
authentication
authorization
jwt
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/streaming-json-parsing-with-a-depth-counter-explained-typescript-0382/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.