<?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:  Oscar Hsieh</title>
    <description>The latest articles on DEV Community by  Oscar Hsieh (@oscarhsieh).</description>
    <link>https://dev.to/oscarhsieh</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%2F4113993%2F001aeeb0-3668-44fa-aa09-74ca9864f98f.png</url>
      <title>DEV Community:  Oscar Hsieh</title>
      <link>https://dev.to/oscarhsieh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oscarhsieh"/>
    <language>en</language>
    <item>
      <title>A thoughtful JSON editor with line flags — the tool I always wished existed for handling JSON payloads.</title>
      <dc:creator> Oscar Hsieh</dc:creator>
      <pubDate>Mon, 07 Sep 2026 13:43:19 +0000</pubDate>
      <link>https://dev.to/oscarhsieh/i-kept-losing-track-of-which-json-lines-i-had-already-changed-d4a</link>
      <guid>https://dev.to/oscarhsieh/i-kept-losing-track-of-which-json-lines-i-had-already-changed-d4a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flthzdb2bz9xlnxcy8717.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flthzdb2bz9xlnxcy8717.png" alt="JsonSilk — the input pane on the left, the output pane on the right with the text, tree, table and mind map views" width="799" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most of my JSON work is editing, not reading.&lt;/p&gt;

&lt;p&gt;A config file comes in, and I have to change fifteen values scattered through it. Or I get a fixture that needs half its records adjusted before it is useful. So my actual loop is: paste, format, edit, minify, copy back out — and then do it again ten minutes later when something else needs fixing.&lt;/p&gt;

&lt;p&gt;The formatting was never the hard part. What kept slowing me down was much dumber than that: &lt;strong&gt;after twenty minutes of editing I could no longer remember which lines I had already dealt with.&lt;/strong&gt; I would scroll up and down re-reading my own changes to check.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://jsonsilk.com" rel="noopener noreferrer"&gt;JsonSilk&lt;/a&gt; around the editing loop instead of around the formatting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Marking the lines I have touched
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1x1ulzo7z3mu387rvzkz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1x1ulzo7z3mu387rvzkz.png" alt="Line flags let you bookmark specific lines to track and jump between your edits quickly." width="800" height="755"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the feature I started with, and it is still the one I use most.&lt;/p&gt;

&lt;p&gt;Click any line number and it gets a flag. The header keeps a count and gives you &lt;strong&gt;prev / next&lt;/strong&gt; to jump between the flagged lines, and a clear to wipe them all. That is the whole feature.&lt;/p&gt;

&lt;p&gt;What makes it work is that &lt;strong&gt;the flags follow the content, not the line number&lt;/strong&gt;. If I flag line 40 and then insert ten lines above it, the flag is on line 50 — still on the thing I marked. Storing line numbers would have been much easier to write and would have been wrong within about thirty seconds of real use.&lt;/p&gt;

&lt;p&gt;It is a scratch annotation layer for a document I will only have open for ten minutes. Nothing clever, and I did not know how much I wanted it until I had it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding things, in both panes at once
&lt;/h2&gt;

&lt;p&gt;The other half of the loop is finding the values I came to change.&lt;/p&gt;

&lt;p&gt;Each pane has its own independent search box, so I can search for different things on each side at the same time. The part that changed how I work is not the box, though — it is that &lt;strong&gt;matching line numbers light up in the gutter&lt;/strong&gt;. In a nine-hundred-line file that turns "where is this key" from scrolling and squinting into a glance.&lt;/p&gt;

&lt;p&gt;Both boxes take a regular expression when you tick &lt;strong&gt;Regex&lt;/strong&gt;, which I added once I noticed I was mostly searching for shapes rather than strings: every ISO timestamp, every empty string, every key ending in &lt;code&gt;_id&lt;/code&gt;. Two details I had to get right:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regex and Match case are independent.&lt;/strong&gt; I nearly made them mutually exclusive, which would have quietly removed "search by pattern, but case-sensitively". With the data &lt;code&gt;{userId, userid, USERID, user_id}&lt;/code&gt; and the pattern &lt;code&gt;user.d&lt;/code&gt;: plain text finds 0, regex case-insensitive finds 4, regex case-sensitive finds 3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A half-typed pattern says so.&lt;/strong&gt; While you are still typing &lt;code&gt;[&lt;/code&gt;, the pattern is invalid — and silently returning zero matches looks exactly like "there is nothing here". So the counter reads &lt;code&gt;invalid&lt;/code&gt;, the box underlines red, and the reason sits in the tooltip.&lt;/p&gt;

&lt;h2&gt;
  
  
  The editing loop itself
&lt;/h2&gt;

&lt;p&gt;Format re-indents with two spaces, four spaces or tabs. Minify goes the other way. Optionally every object's keys get sorted recursively, which is what I use when I want to diff two versions of the same config.&lt;/p&gt;

&lt;p&gt;The one that removes the most steps is &lt;strong&gt;Move to INPUT&lt;/strong&gt;. You paste a minified blob, format it, and the readable version is now in the right-hand pane — which is read-only. To keep working on it you would have to select, copy, click left, select all, paste. So there is a button that makes the formatted output your new input, cursor at the top. Formatting something already formatted is a no-op, so you can just carry on.&lt;/p&gt;

&lt;p&gt;When the JSON is broken mid-edit — which, when you are editing by hand, is most of the time — two things happen that I built for myself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The error message is a link.&lt;/strong&gt; Click &lt;code&gt;Line 4, Column 27&lt;/code&gt; and the cursor lands on that character in the input, focused, ready to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The output keeps the last valid result&lt;/strong&gt; instead of going blank, and says that is what it is doing. Watching my work disappear every time I fat-fingered a bracket was the single most annoying thing about my old loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways to look at the same document
&lt;/h2&gt;

&lt;p&gt;Once the editing was comfortable, the thing I wanted next was different ways to &lt;em&gt;see&lt;/em&gt; the document. There are four views and they share one search box, so a pattern you wrote while reading the raw JSON keeps working when you switch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text&lt;/strong&gt; is the formatted JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tree&lt;/strong&gt; collapses and expands. When you search, branches that have matches &lt;em&gt;inside&lt;/em&gt; them get marked rather than auto-expanded — so the shape of the document does not jump around while you type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table&lt;/strong&gt; lays an array of objects out as rows and columns. It is the same projection the CSV export uses, so what you see is what you would save. It loads two thousand rows at a time with the remaining count written on the button, and searching still covers every row, loaded or not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mind map&lt;/strong&gt; draws the document as cards joined by labelled lines: one card per object or array, one row per field, a line out to every nested object with its key on it. It answers a different question from the tree — not "what is inside this branch" but "how is this document shaped". Scroll to move, &lt;code&gt;Ctrl&lt;/code&gt;+scroll to zoom, drag the empty canvas to pan, press &lt;code&gt;0&lt;/code&gt; to fit.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd7sepe3cglrq347kkfe7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd7sepe3cglrq347kkfe7.png" alt="Mind map" width="740" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wrote the mind map myself rather than pulling in a graph library. The layout is two iterative passes, no recursion, and the cards are ordinary HTML absolutely positioned over an SVG layer that draws the edges — which means the values stay selectable text in the same colours the other views use.&lt;/p&gt;

&lt;p&gt;That paid off in a way I did not plan. Because the cards are real elements, I could put &lt;code&gt;content-visibility: auto&lt;/code&gt; on each one with the exact height the layout had already computed, so cards outside the viewport are skipped entirely. A 3,000-row document draws in 107 ms in the mind map against 267 ms in the tree — the mind map ended up being the faster of the two, which still surprises me.&lt;/p&gt;

&lt;p&gt;You can also save the map as SVG or PNG. That one had a trap in it: the obvious approach is to screenshot the DOM, and that produces a &lt;strong&gt;wrong image&lt;/strong&gt;, silently — &lt;code&gt;content-visibility: auto&lt;/code&gt; means the off-screen cards have never been laid out or painted, so anything reading the DOM gets a pile of empty boxes and no error. The export re-draws from the layout data instead, so it comes out complete regardless of what is scrolled into view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Colours I can look at all afternoon
&lt;/h2&gt;

&lt;p&gt;This is not a technical feature but it is the thing I spent the most fiddly time on.&lt;/p&gt;

&lt;p&gt;Nothing in the interface is pure white or pure black. The code surface is an off-white &lt;code&gt;#F6F9F4&lt;/code&gt; and the text on it is &lt;code&gt;#232922&lt;/code&gt; — near-black, not &lt;code&gt;#000&lt;/code&gt;. The page around it is a muted sage. The dark theme is a deep green &lt;code&gt;#12211D&lt;/code&gt; rather than black, for the same reason in the other direction.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz974uf9jssot55vcyars.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz974uf9jssot55vcyars.png" alt="Dark mode" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The syntax colours are deliberately low-saturation too: keys, strings, numbers and booleans are distinguishable at a glance without any of them shouting. And &lt;strong&gt;the same four colours are used in all four views&lt;/strong&gt;, so a string looks like a string whether you are in the text, tree, table or mind map.&lt;/p&gt;

&lt;p&gt;The JSON text also has its own size control in the header, separate from browser zoom, so the code can get bigger without the rest of the page doing the same.&lt;/p&gt;

&lt;p&gt;I have no data to prove any of this reduces eye strain. I only know that I stopped noticing the interface, which is what I was aiming for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Big files stay editable
&lt;/h2&gt;

&lt;p&gt;I wanted a large fixture to still be something I could type in, not just view.&lt;/p&gt;

&lt;p&gt;Parsing and printing run in a Web Worker, so the main thread stays free. Only the rows on screen exist in the DOM — the document height is faked with a spacer — so redrawing a 60 MB document costs about the same as a 60 KB one. The re-format delay also grows with document size, so it never fights your typing on a big file.&lt;/p&gt;

&lt;p&gt;I have tested to &lt;strong&gt;80 MB and 7.5 million lines&lt;/strong&gt;. Somewhere between 80 and 100 MB the tab runs out of memory and crashes; peak memory is roughly eight times the file size, which is a browser ceiling rather than something better code fixes.&lt;/p&gt;

&lt;p&gt;The tree and table views are more expensive than text — about 1.2 seconds per MB against 0.11 — because they build a second projection and move it back across the worker boundary. So past a few megabytes the tool gives you an estimate in seconds and lets you decide, rather than freezing and hoping.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing I found on the way
&lt;/h2&gt;

&lt;p&gt;I did not set out to write a parser. I was going to use &lt;code&gt;JSON.parse&lt;/code&gt; like anyone would, and I started there.&lt;/p&gt;

&lt;p&gt;Then I noticed a twenty-digit ID coming back as &lt;code&gt;12345678901234567000&lt;/code&gt;. &lt;code&gt;JSON.parse&lt;/code&gt; → operate → &lt;code&gt;JSON.stringify&lt;/code&gt; is lossy: &lt;code&gt;1e400&lt;/code&gt; becomes &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;-0&lt;/code&gt; becomes &lt;code&gt;0&lt;/code&gt;, and long decimals get rounded. For a tool whose entire job is to show you your own data and hand it back, that was not acceptable.&lt;/p&gt;

&lt;p&gt;So every scalar keeps its &lt;strong&gt;original literal text&lt;/strong&gt;, and the printer writes that text back out. Nothing is ever converted to a JavaScript number and back. The YAML, CSV and XML exports read the same raw field, so they are not lossy either.&lt;/p&gt;

&lt;p&gt;Writing a JSON parser is not clever — it is a known, bounded problem. It is just work that has to happen up front, because retrofitting it means rewriting the whole pipeline. The unexpected payoff was error messages: since I am walking the text myself, I know exactly where I stopped and what I expected, so a trailing comma can say &lt;em&gt;trailing comma&lt;/em&gt; instead of &lt;em&gt;unexpected token&lt;/em&gt;.&lt;/p&gt;

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

&lt;p&gt;No server, so no saved documents and no sharing links. No JSON Schema validation. No diff view — which is the one I most expect to want next. The table only understands arrays of objects, because that is the shape that flattens honestly.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://jsonsilk.com" rel="noopener noreferrer"&gt;jsonsilk.com&lt;/a&gt; — no account, no upload. There is no server; it is a static page, and nothing you paste leaves the tab.&lt;/p&gt;

&lt;p&gt;Every feature above exists because the loop I described at the top was two steps longer than it should have been. If something in your JSON workflow is like that, I would like to hear about it.&lt;/p&gt;

</description>
      <category>json</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
