<?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>Forem: valentin CONAN</title>
    <description>The latest articles on Forem by valentin CONAN (@valentinconan).</description>
    <link>https://forem.com/valentinconan</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3852218%2F8e26b60f-e4a8-4428-92e6-cc913a6a531f.jpg</url>
      <title>Forem: valentin CONAN</title>
      <link>https://forem.com/valentinconan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/valentinconan"/>
    <language>en</language>
    <item>
      <title>I built a JSON viewer because the most popular one betrayed its users</title>
      <dc:creator>valentin CONAN</dc:creator>
      <pubDate>Thu, 02 Apr 2026 20:43:33 +0000</pubDate>
      <link>https://forem.com/valentinconan/i-built-a-json-viewer-because-the-most-popular-one-betrayed-its-users-5e6e</link>
      <guid>https://forem.com/valentinconan/i-built-a-json-viewer-because-the-most-popular-one-betrayed-its-users-5e6e</guid>
      <description>&lt;h2&gt;
  
  
  The incident
&lt;/h2&gt;

&lt;p&gt;In January 2026, JSON Formatter — the Chrome extension used by 2M+ developers — pushed an update that injected a donation popup from a service called "Give Freely" on checkout pages. Without warning. Without consent.&lt;/p&gt;

&lt;p&gt;Developers reported seeing unexpected UI while entering credit card information.&lt;br&gt;
Some thought their browser was compromised. Security teams flagged it internally.&lt;br&gt;
Hundreds of 1-star reviews followed within days.&lt;/p&gt;

&lt;p&gt;I'd been using JSON Formatter daily for years. Like most of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision
&lt;/h2&gt;

&lt;p&gt;I'd been wanting to build a better JSON viewer for a while. The landscape was frustrating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSON Formatter&lt;/strong&gt;: great basics, but minimal features, and now... this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON Viewer (tulios)&lt;/strong&gt;: richest feature set, but last updated December 2020. Not MV3 compatible. Breaking in newer Chrome versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No extension&lt;/strong&gt; handles JSON, YAML and XML in the same tool natively.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Give Freely incident was the signal I needed. I started building in January 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JSONVault Pro&lt;/strong&gt; is a Chrome extension that turns any JSON, YAML or XML URL into a clean, interactive, searchable tree. Instantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features I'm most proud of
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;JWT auto-decode&lt;/strong&gt;&lt;br&gt;
When a string value starts with &lt;code&gt;eyJ&lt;/code&gt;, the extension detects it as a JWT and shows the decoded header + payload inline. No copy-paste, no jwt.io tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Base64 auto-detect&lt;/strong&gt;&lt;br&gt;
Any Base64-encoded string is detected and decoded inline. This was my personal "oh wow" moment — I was inspecting an API response, clicked decode on a value, and an image appeared directly in the JSON tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Side-by-side diff&lt;/strong&gt;&lt;br&gt;
A proper LCS diff engine — not just line comparison. It handles key-order differences, type coercions (number vs string), null vs absent keys.&lt;br&gt;
Each change group is navigable with ↑/↓ shortcuts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSONPath queries&lt;/strong&gt;&lt;br&gt;
Filter and extract data directly in the browser. No jq, no terminal, no online tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtual rendering&lt;/strong&gt;&lt;br&gt;
Files over 10MB are rendered with a virtual tree (only visible rows in DOM).&lt;br&gt;
I've tested up to 50MB JSON files without freezing the tab.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stack
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;TypeScript + Preact&lt;/strong&gt; instead of React. Preact is 3KB gzipped vs React's 45KB. For a content script injected on every JSON page, bundle size matters — every KB is parsed on the main thread at injection time. Preact is API-compatible with React,&lt;br&gt;
so the DX is identical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vite + CRXJS&lt;/strong&gt; for the build. CRXJS is a Vite plugin that handles the MV3 manifest transformation automatically — it rewrites asset paths, generates the service worker loader, handles HMR for extension pages. Without it, you're writing custom Rollup plugins and debugging path issues for days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shadow DOM&lt;/strong&gt; for the content script. This was the most important architectural decision. When a content script injects UI directly into a page, it fights CSS specificity wars with the host page forever — the host page's styles leak in, your styles leak out. Shadow DOM gives you a completely isolated CSS environment.&lt;br&gt;
Design tokens, themes, all of it works cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic injection&lt;/strong&gt; instead of static &lt;code&gt;content_scripts&lt;/code&gt; in the manifest. Static injection runs your script on every page load matching &lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt;. Dynamic injection (via &lt;code&gt;chrome.scripting.executeScript&lt;/code&gt;) fires only after the service worker&lt;br&gt;
confirms the Content-Type is JSON/YAML/XML. Zero performance impact on non-JSON pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zustand&lt;/strong&gt; for state. Redux is overkill for an extension. Zustand is ~1KB, has zero boilerplate, and works fine with Preact. The viewer store, devtools store, and settings store are all independent Zustand slices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSS Custom Properties&lt;/strong&gt; for theming. No runtime CSS-in-JS. All 26 themes (5 free + 21 Pro) are pure CSS files with custom property overrides. Switching themes is a single attribute change on the host element — &lt;code&gt;data-theme="dracula"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web Workers&lt;/strong&gt; for large files. Parsing a 5MB JSON on the main thread blocks the tab for 2-3 seconds. Anything above 5MB goes to a Worker. The tricky part: Web Workers in content scripts can't use &lt;code&gt;new Worker(url)&lt;/code&gt; because the extension URL isn't accessible from the page context. The solution is &lt;code&gt;?worker&amp;amp;inline&lt;/code&gt; in Vite, which bundles the Worker as a blob URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ExtensionPay&lt;/strong&gt; for payments. Stripe without a backend. The payment flow is: user clicks "Upgrade" → ExtensionPay opens a Stripe checkout page → on success, a content script on &lt;code&gt;extensionpay.com/stripe-payment-succeeded&lt;/code&gt; fires a message to the service worker → license cached locally in &lt;code&gt;chrome.storage.local&lt;/code&gt;.&lt;br&gt;
No server, no webhook, no infra to maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  The freemium model
&lt;/h3&gt;

&lt;p&gt;Free tier covers everything a developer needs daily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON, YAML, XML formatting&lt;/li&gt;
&lt;li&gt;Auto mode + Light, Dark, Sepia, High Contrast themes&lt;/li&gt;
&lt;li&gt;Collapsible tree with counts&lt;/li&gt;
&lt;li&gt;Full-text search&lt;/li&gt;
&lt;li&gt;Copy JSONPath&lt;/li&gt;
&lt;li&gt;Sort keys&lt;/li&gt;
&lt;li&gt;Diff tool (basic)&lt;/li&gt;
&lt;li&gt;DevTools panel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pro adds the power features: JWT decode, advanced diff, JSONPath, export to CSV/YAML/XML/TypeScript, document history, 21 premium themes, virtual rendering for huge files, live editing.&lt;/p&gt;

&lt;p&gt;Zero tracking on both tiers. No analytics, no telemetry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Shadow DOM was the right call.&lt;/strong&gt; Content scripts that inject UI directly into pages fight CSS specificity wars with the host page forever. Shadow DOM gives you a clean isolated environment. Worth the extra complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web Workers are mandatory for large files.&lt;/strong&gt; Parsing a 5MB JSON on the main thread freezes the tab. I learned this the hard way. The threshold where I switch to a Worker is 5MB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The freemium gate has to be visible, not hidden.&lt;/strong&gt; I show locked Pro features with a 🔒 badge and a tooltip explaining what they do. The user sees the feature, understands the value, and can decide to upgrade. Hiding Pro features entirely means users never know they exist.&lt;/p&gt;

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

&lt;p&gt;Chrome Web Store: &lt;a href="https://chromewebstore.google.com/detail/ipghignebclihkckdpcagnicpopjaokj" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/ipghignebclihkckdpcagnicpopjaokj&lt;/a&gt;&lt;br&gt;
Website: &lt;a href="https://jsonvaultpro.com" rel="noopener noreferrer"&gt;https://jsonvaultpro.com&lt;/a&gt;&lt;br&gt;
Feature requests &amp;amp; bugs: &lt;a href="https://github.com/valentinconan/jsonvault-pro-site/issues" rel="noopener noreferrer"&gt;https://github.com/valentinconan/jsonvault-pro-site/issues&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've built a Chrome extension before, I'd love to hear how you handled the MV3 migration — especially around &lt;code&gt;webRequest&lt;/code&gt; vs &lt;code&gt;declarativeNetRequest&lt;/code&gt;.&lt;br&gt;
And if you're still on JSON Formatter, I'd genuinely like to know what would make you switch.&lt;/p&gt;

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