<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Manthan Kansagra</title>
    <description>The latest articles on DEV Community by Manthan Kansagra (@acegikmo135).</description>
    <link>https://dev.to/acegikmo135</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4131808%2Fc064fcc0-eaa6-4cf9-90a7-041300c44e87.png</url>
      <title>DEV Community: Manthan Kansagra</title>
      <link>https://dev.to/acegikmo135</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/acegikmo135"/>
    <language>en</language>
    <item>
      <title>JSON.parse throws on every token your LLM streams. Here's a 425-byte fix.</title>
      <dc:creator>Manthan Kansagra</dc:creator>
      <pubDate>Fri, 18 Sep 2026 16:26:24 +0000</pubDate>
      <link>https://dev.to/acegikmo135/jsonparse-throws-on-every-token-your-llm-streams-heres-a-425-byte-fix-1cne</link>
      <guid>https://dev.to/acegikmo135/jsonparse-throws-on-every-token-your-llm-streams-heres-a-425-byte-fix-1cne</guid>
      <description>&lt;p&gt;I was building a small app where an LLM returns a recipe as JSON and the UI fills in as the answer streams. Simple idea. It fell over immediately.&lt;/p&gt;

&lt;p&gt;Here's what my handler actually saw between tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pad Th
{"&lt;/span&gt;&lt;span class="err"&gt;title&lt;/span&gt;&lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="err"&gt;Pad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Thai&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="err"&gt;ingr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pad Thai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ingredients"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rice noo
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JSON.parse&lt;/code&gt; throws on every one of those. Every single token, until the very last &lt;code&gt;}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So you have two bad options. Wait for the whole thing to finish — which throws away the reason you're streaming in the first place. Or write a regex that closes some brackets and hope. Mine broke on the first escaped quote.&lt;/p&gt;

&lt;p&gt;I wanted a third option: on every chunk, give me the best value you can honestly get from what's arrived so far. Never throw. Never make things up.&lt;/p&gt;

&lt;p&gt;That turned into a tiny library called &lt;strong&gt;SoFar&lt;/strong&gt;. It's 425 bytes gzipped, has zero dependencies, and does exactly one thing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssyj8h97n0r5f17hbtdj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssyj8h97n0r5f17hbtdj.gif" alt="A JSON document arriving token by token, with the parsed value updating beside it" width="621" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parsePartialJSON&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sofar-json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;parsePartialJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"title": "Pad Th&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → { title: "Pad Th" }&lt;/span&gt;

&lt;span class="nf"&gt;parsePartialJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"title": "Pad Thai", "ingr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → { title: "Pad Thai" }&lt;/span&gt;

&lt;span class="nf"&gt;parsePartialJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"title": "Pad Thai", "servings": 4,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → { title: "Pad Thai", servings: 4 }&lt;/span&gt;

&lt;span class="nf"&gt;parsePartialJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"ok": tru&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → {}&lt;/span&gt;

&lt;span class="nf"&gt;parsePartialJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the fourth one. &lt;code&gt;tru&lt;/code&gt; is &lt;em&gt;probably&lt;/em&gt; going to be &lt;code&gt;true&lt;/code&gt;, but the buffer doesn't say that yet, so you get the last value that was actually complete. A partial string comes back as a string, because a prefix of a string is still a string. A partial number or literal doesn't get guessed.&lt;/p&gt;

&lt;p&gt;For the streaming case there's a small stateful wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createJSONStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sofar-json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createJSONStream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;llmResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// strict parse once it's done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole API. Two functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;I didn't want a second JSON parser. &lt;code&gt;JSON.parse&lt;/code&gt; is fast, correct, and already in every runtime. I just needed to hand it something it could accept.&lt;/p&gt;

&lt;p&gt;So the library scans the buffer once, left to right, keeping track of three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which containers are open — a stack of &lt;code&gt;{&lt;/code&gt; and &lt;code&gt;[&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Whether we're inside a string, and whether the last character was a backslash&lt;/li&gt;
&lt;li&gt;A list of &lt;strong&gt;safe cut points&lt;/strong&gt;: positions where the JSON so far is structurally sound. Right after a string closes, right after a container opens or closes, right before a comma.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each cut point stores a snapshot of the stack at that moment. Not a copy — the stack is kept as the string of closing brackets you'd need to append, innermost first, so a snapshot is just a reference. Rewinding later is a slice and a concat. No rescanning.&lt;/p&gt;

&lt;p&gt;Then it tries two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1.&lt;/strong&gt; If we're inside a string, close it. Append the closers for every open container. &lt;code&gt;JSON.parse&lt;/code&gt;. This handles almost everything: a string cut mid-word, an array mid-element, an object mid-value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2.&lt;/strong&gt; If that fails — a dangling key like &lt;code&gt;{"a": 1, "b&lt;/code&gt;, a trailing comma, a half-typed &lt;code&gt;fals&lt;/code&gt; — walk the cut points backwards. Slice the buffer at each one, append that cut's stored closers, try again. The first one that parses wins.&lt;/p&gt;

&lt;p&gt;If nothing parses, return &lt;code&gt;undefined&lt;/code&gt;. Never throw.&lt;/p&gt;

&lt;p&gt;There's a bit of extra care for escapes. A trailing &lt;code&gt;\&lt;/code&gt; gets dropped, a half-finished &lt;code&gt;\u00e&lt;/code&gt; gets trimmed back to the backslash, but &lt;code&gt;\\&lt;/code&gt; at the end of a string is left alone because that's a complete escape. The scanner already knows which case it's in, so this costs nothing.&lt;/p&gt;

&lt;p&gt;The whole thing is one pass, O(n), and on a 1.6 MB buffer it runs at about 1.3× the cost of a bare &lt;code&gt;JSON.parse&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it compares
&lt;/h2&gt;

&lt;p&gt;I checked the alternatives with the same inputs before writing this. Sizes are esbuild + gzip.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;SoFar&lt;/th&gt;
&lt;th&gt;partial-json&lt;/th&gt;
&lt;th&gt;best-effort-json-parser&lt;/th&gt;
&lt;th&gt;jsonrepair&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Size&lt;/td&gt;
&lt;td&gt;425 B&lt;/td&gt;
&lt;td&gt;1.6 kB&lt;/td&gt;
&lt;td&gt;1.9 kB&lt;/td&gt;
&lt;td&gt;3.7 kB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;''&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;throws&lt;/td&gt;
&lt;td&gt;&lt;code&gt;""&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;throws&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;}}}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;throws&lt;/td&gt;
&lt;td&gt;returns &lt;code&gt;"}}}"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;throws&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{"ok": tru&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ok: true}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ok: true}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ok: "tru"}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{"a":"x","ingr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{a: "x"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{a: "x"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{a: "x"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{a: "x", ingr: null}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They're all fine libraries and I'd used a couple of them. What I wanted was different: never throw, never invent, and be small enough that I'd never think about it in a bundle. If your JSON has broken &lt;em&gt;syntax&lt;/em&gt; — single quotes, comments, unquoted keys — &lt;code&gt;jsonrepair&lt;/code&gt; is the right tool, and the two chain nicely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it won't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It won't fix JSON that was never going to be valid.&lt;/li&gt;
&lt;li&gt;It won't guess &lt;code&gt;4.&lt;/code&gt; is &lt;code&gt;4&lt;/code&gt;. That could be &lt;code&gt;4.5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It won't give you diffs. You get a full value each call. At LLM token rates that's fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;There's a playground where you can paste any JSON and scrub through it a character at a time. It shows the exact string handed to &lt;code&gt;JSON.parse&lt;/code&gt;, which characters were synthesized, which were rewound past, and every cut point the scanner recorded:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://acegikmo135.github.io/sofar/" rel="noopener noreferrer"&gt;https://acegikmo135.github.io/sofar/&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;sofar-json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source, tests, and the fuzz test that feeds a document one character at a time are on GitHub: &lt;strong&gt;&lt;a href="https://github.com/acegikmo135/sofar" rel="noopener noreferrer"&gt;https://github.com/acegikmo135/sofar&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've got a stream it handles badly, the most useful thing you can send me is the raw buffer as a string. It becomes a test case.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
