The incident
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.
Developers reported seeing unexpected UI while entering credit card information.
Some thought their browser was compromised. Security teams flagged it internally.
Hundreds of 1-star reviews followed within days.
I'd been using JSON Formatter daily for years. Like most of you.
The decision
I'd been wanting to build a better JSON viewer for a while. The landscape was frustrating:
- JSON Formatter: great basics, but minimal features, and now... this.
- JSON Viewer (tulios): richest feature set, but last updated December 2020. Not MV3 compatible. Breaking in newer Chrome versions.
- No extension handles JSON, YAML and XML in the same tool natively.
The Give Freely incident was the signal I needed. I started building in January 2026.
What I built
JSONVault Pro is a Chrome extension that turns any JSON, YAML or XML URL into a clean, interactive, searchable tree. Instantly.
Features I'm most proud of
JWT auto-decode
When a string value starts with eyJ, the extension detects it as a JWT and shows the decoded header + payload inline. No copy-paste, no jwt.io tab.
Base64 auto-detect
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.
Side-by-side diff
A proper LCS diff engine — not just line comparison. It handles key-order differences, type coercions (number vs string), null vs absent keys.
Each change group is navigable with ↑/↓ shortcuts.
JSONPath queries
Filter and extract data directly in the browser. No jq, no terminal, no online tools.
Virtual rendering
Files over 10MB are rendered with a virtual tree (only visible rows in DOM).
I've tested up to 50MB JSON files without freezing the tab.
The stack
TypeScript + Preact 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,
so the DX is identical.
Vite + CRXJS 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.
Shadow DOM 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.
Design tokens, themes, all of it works cleanly.
Dynamic injection instead of static content_scripts in the manifest. Static injection runs your script on every page load matching <all_urls>. Dynamic injection (via chrome.scripting.executeScript) fires only after the service worker
confirms the Content-Type is JSON/YAML/XML. Zero performance impact on non-JSON pages.
Zustand 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.
CSS Custom Properties 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 — data-theme="dracula".
Web Workers 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 new Worker(url) because the extension URL isn't accessible from the page context. The solution is ?worker&inline in Vite, which bundles the Worker as a blob URL.
ExtensionPay 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 extensionpay.com/stripe-payment-succeeded fires a message to the service worker → license cached locally in chrome.storage.local.
No server, no webhook, no infra to maintain.
The freemium model
Free tier covers everything a developer needs daily:
- JSON, YAML, XML formatting
- Auto mode + Light, Dark, Sepia, High Contrast themes
- Collapsible tree with counts
- Full-text search
- Copy JSONPath
- Sort keys
- Diff tool (basic)
- DevTools panel
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.
Zero tracking on both tiers. No analytics, no telemetry.
Lessons learned
Shadow DOM was the right call. 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.
Web Workers are mandatory for large files. 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.
The freemium gate has to be visible, not hidden. 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.
Try it
Chrome Web Store: https://chromewebstore.google.com/detail/ipghignebclihkckdpcagnicpopjaokj
Website: https://jsonvaultpro.com
Feature requests & bugs: https://github.com/valentinconan/jsonvault-pro-site/issues
If you've built a Chrome extension before, I'd love to hear how you handled the MV3 migration — especially around webRequest vs declarativeNetRequest.
And if you're still on JSON Formatter, I'd genuinely like to know what would make you switch.
Top comments (0)