JSON6 in the Browser Guide

Load the JSON6 parser with a script tag, an ES module or a bundler. Includes the global-name trap in the minified build and how to work around it.

TL;DR: Copy node_modules/json-6/dist/index.min.mjs next to your site and import it as a module for the smallest, cleanest option. If you need a classic script tag, dist/index.min.js works but it registers the global as JSON5, not JSON6; add window.JSON6 = window.JSON5 after loading it, or load lib/json6.js instead.

Which File to Load

The json-6 package contains several builds of the same parser. Which one you want depends on whether you use a script tag, an ES module or a bundler. All sizes below are measured from the 1.1.7 package; gzip sizes are approximate.

FileFormatSize (gzip)How it exposes the parser
dist/index.min.mjsES module, minified16,310 bytes (5,628)export default the JSON6 object. No global.
dist/index.mjsES module, readable79,312 bytesexport default the JSON6 object.
dist/index.min.jsUMD, minified, with polyfills32,046 bytes (11,724)CommonJS, AMD, or the global JSON5.
dist/index.jsUMD, readable141,213 bytesSame as above, global JSON5.
lib/json6.jsPlain script / CommonJS62,660 bytes (15,441)Top-level const JSON6 in a script tag; module.exports under Node.

The package README recommends loading lib/json6.js with a script tag and says the object JSON6 becomes global. It also says the minified UMD build is meant to run on very old platforms, which is why that file bundles polyfills for string code point methods and is twice the size of the ES module. For anything modern, prefer the ES module.

There is no CDN link on this page on purpose. Copy the file from node_modules into your own static assets and serve it from your own origin, then pin the version by file name. That keeps a third party out of your page, works under a strict Content Security Policy and removes a request to another host.

The Global Is Called JSON5

This is the one thing that will trip you up. The UMD wrapper in both dist/index.js and dist/index.min.js ends with a "no module system" branch that assigns the result to global.JSON5. The first characters of the minified file, abbreviated, are:

!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).JSON5=t()}(this,function(){ ... })

The code inside is the JSON6 parser (its version is "1.1.7") but the name it is published under in a browser is JSON5. The file does not define window.JSON6 at all. The package's own README tells you the opposite for the script-tag case, but its example uses lib/json6.js, not the dist bundles. Under CommonJS, AMD and bundlers the wrapper uses module.exports or define and the name does not matter, which is why this only shows up with a plain <script src>.

I loaded each file into headless Chromium through Playwright, with no module system present, and recorded what ended up global.

File loaded with a script tagwindow.JSON6window.JSON5Bare JSON6 identifier
dist/index.min.jsundefinedthe JSON6 parserReferenceError
dist/index.jsundefinedthe JSON6 parserReferenceError
lib/json6.jsundefinedundefinedworks (global lexical binding)
dist/index.min.mjs (as a module)undefinedundefinednot applicable, import it

The lib/json6.js row needs a note. It declares the object with a top-level const. In a classic script that creates a global lexical binding: later scripts can write JSON6.parse(...) and it works, but the property is not on window, so window.JSON6 and globalThis.JSON6 are both undefined. If you test with typeof window.JSON6 you will wrongly conclude it failed to load.

Workaround 1: alias it

Add one line right after the script tag. This is the simplest and I used it in the demo below.

<script src="/vendor/index.min.js"></script> <script> window.JSON6 = window.JSON5; </script>

Workaround 2: keep a real JSON5 intact

If your page also uses the JSON5 library, the bundle overwrites it. Save the old value first and restore it after. I verified this order in Chromium: afterwards JSON6.parse is a function and the original JSON5 object is untouched.

<script>var previousJSON5 = window.JSON5;</script> <script src="/vendor/index.min.js"></script> <script> window.JSON6 = window.JSON5; window.JSON5 = previousJSON5; </script>

Workaround 3: avoid the UMD build

Use the ES module build, or the plain lib/json6.js script. Both avoid the naming problem entirely. The next section shows the module route.

If you copy a browser example written for JSON5 and swap the library, the global name JSON5 will appear to work with dist/index.min.js because of this behavior, and break the day the package fixes the name. Alias it explicitly instead of relying on the accident.

ES Module (Recommended)

Serve dist/index.min.mjs as a static file and import it by URL. It has a single default export, so there is no global to collide with, and it is the smallest build. Remember that module scripts are deferred and are subject to CORS, so use a real HTTP server, not a file:// URL.

<script type="module"> import JSON6 from '/vendor/json-6.min.mjs'; const config = JSON6.parse("{ theme: 'dark', sidebar: true, }"); console.log(config.theme); </script>

In my Chromium test the module imported cleanly from an HTTPS origin, parsed a document with a back-tick string, and created neither window.JSON6 nor window.JSON5. An import map can map the bare specifier json-6 to the file, so your own modules write import JSON6 from 'json-6' exactly as they do in Node. I tested the map below in Chromium and it printed the version and parsed a document.

<script type="importmap"> { "imports": { "json-6": "/vendor/json-6.min.mjs" } } </script> <script type="module"> import JSON6 from 'json-6'; console.log(JSON6.version); </script>

A Complete Page

This is a full working page with no inline event handlers: a text area, a button, and a script that parses the text and shows JSON. It uses the global alias workaround and the minified UMD build copied to /vendor/index.min.js. I served it to headless Chromium with a Content Security Policy of script-src https://example.test plus inline scripts, so no unsafe-eval, and clicking the button produced the output below with no console messages.

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSON6 in the browser</title> </head> <body> <textarea id="input" rows="6" cols="50">{ // edit me name: 'demo', ports: [80, 0x1BB,], }</textarea> <button id="convert" type="button">Convert to JSON</button> <pre id="output"></pre> <script src="/vendor/index.min.js"></script> <script> // dist/index.min.js registers the global as JSON5 (see the notes below). window.JSON6 = window.JSON5; document.getElementById('convert').addEventListener('click', function () { var out = document.getElementById('output'); try { var value = JSON6.parse(document.getElementById('input').value); out.textContent = JSON.stringify(value, null, 2); } catch (err) { out.textContent = 'Error: ' + err.message; } }); </script> </body> </html>
ActionResult in Chromium
Click with the default text{ "name": "demo", "ports": [80, 443] } as indented JSON (the hex number and trailing comma parsed)
Replace the text with { a: 1,, } and clickError: bad context; excessive commas while parsing; ',' unexpected at 8 ... [1:8]

The parser never calls eval or new Function, so it needs no relaxed CSP. That is a real advantage over the habit of pasting JavaScript object literals into eval.

Fetching a .json6 File

To load configuration at runtime, fetch the file as text and parse it. Use response.text(), never response.json(), because json() calls the native strict parser and fails on comments. The response MIME type does not matter for text(): in my test the server answered with application/octet-stream, which is what many static hosts send for an unknown extension, and the fetch worked.

async function loadConfig(url) { const response = await fetch(url); if (!response.ok) throw new Error('HTTP ' + response.status + ' for ' + url); return JSON6.parse(await response.text()); } loadConfig('/config.json6').then(function (config) { console.log(config.title); });

In Chromium this printed the title for a file containing { title: 'From fetch', items: [1,2,3,], }, with lib/json6.js loaded by a script tag and used through the bare JSON6 identifier. Host-specific notes: make sure your host serves the file at all (some hosts refuse unknown extensions), and remember that browsers cache it like any other static file.

If you control the build, you can avoid shipping the parser to browsers altogether. Parse .json6 at build time and emit plain JSON, as shown in the bundler section below, or with the command line tool.

Bundlers

Bundlers read the package exports map. The map lists the browser condition first, and it points at dist/index.js, the UMD build with polyfills. That means a default import JSON6 from 'json-6' in a browser bundle pulls in the large build, not the small ES module. I measured the same one-line program under esbuild 0.28 with minification:

browser-sizes.mjs

import * as esbuild from 'esbuild'; import { gzipSync } from 'node:zlib'; const variants = [ ['import from json-6', "import J from 'json-6'; console.log(J.parse('{a:1}').a)"], ['json-6/dist/index.min.mjs', "import J from 'json-6/dist/index.min.mjs'; console.log(J.parse('{a:1}').a)"], ]; for (const [label, contents] of variants) { const result = await esbuild.build({ stdin: { contents, resolveDir: process.cwd() }, bundle: true, platform: 'browser', format: 'esm', minify: true, write: false, logLevel: 'silent', }); const bytes = Buffer.from(result.outputFiles[0].contents); console.log(label.padEnd(28), bytes.length, 'bytes,', gzipSync(bytes).length, 'gzip'); }

Run it:

node browser-sizes.mjs

Output:

import from json-6 33973 bytes, 12659 gzip json-6/dist/index.min.mjs 16235 bytes, 5767 gzip

If size matters, import the minified ES module by its explicit path as in the second row; the package allows json-6/dist/* in its exports. The package does not map types for that deep path, so in TypeScript expect to add a small declaration or a path alias. For most apps the 17 KB difference is not worth the trouble.

Importing .json6 files: esbuild

No bundler understands .json6 by itself, so you write a tiny plugin that parses the file at build time and hands the bundler plain JSON. The parser then does not ship to the browser at all unless you import it elsewhere. This runs on esbuild 0.28.

browser-esbuild.mjs

import * as esbuild from 'esbuild'; import JSON6 from 'json-6'; import { readFile } from 'node:fs/promises'; import vm from 'node:vm'; const json6Plugin = { name: 'json6', setup(build) { build.onLoad({ filter: /\.json6$/ }, async (args) => { const text = await readFile(args.path, 'utf8'); return { contents: JSON.stringify(JSON6.parse(text)), loader: 'json' }; }); }, }; const result = await esbuild.build({ stdin: { contents: "import settings from './settings.json6'; console.log(settings.title, settings.port);", resolveDir: process.cwd() }, bundle: true, platform: 'browser', format: 'iife', write: false, plugins: [json6Plugin], logLevel: 'silent', }); const code = result.outputFiles[0].text; console.log('bundle mentions JSON6 parser:', code.includes('esStrictCompatible')); vm.runInNewContext(code, { console });

Run it:

node browser-esbuild.mjs

Output:

bundle mentions JSON6 parser: false Bundled 8080

Importing .json6 files: Rollup

Rollup's official JSON plugin only handles the .json extension and calls JSON.parse, so write a transform of your own. This runs on Rollup 4.

browser-rollup.mjs

import { rollup } from 'rollup'; import JSON6 from 'json-6'; const json6Plugin = { name: 'json6', transform(code, id) { if (!id.endsWith('.json6')) return null; return { code: `export default ${JSON.stringify(JSON6.parse(code))};`, map: { mappings: '' } }; }, }; const bundle = await rollup({ input: 'rollup-main.js', plugins: [json6Plugin] }); const { output } = await bundle.generate({ format: 'es' }); console.log(output[0].code.trim());

Run it:

node browser-rollup.mjs

Output:

var settings = {"name":"demo","port":8080}; console.log(settings.name, settings.port);

The same idea carries over to other bundlers: read the file, call JSON6.parse, and give back JSON or an ES module that exports it. I only ran esbuild and Rollup for this page. Webpack, Vite and Parcel were not tested, and I found no maintained plugin for JSON6 for any of them, so do not expect a ready-made loader. A search of the npm registry for json6 returns the parser itself, an old require hook and a handful of unrelated packages.

Browser Notes and Limits

TopicDetail
Browser supportThe parser uses let, const and codePointAt. The README says the minified UMD build "should run on very old platforms". I tested only current Chromium.
Global namedist/index.js and dist/index.min.js register JSON5. Alias it or use another build.
CSPNo eval or new Function. Works with a CSP that has no unsafe-eval (tested).
Web WorkersNot tested. The code has no DOM dependency, so a module worker should be able to import the ES module build.
StreamingThe incremental parser JSON6.begin is part of every build. See the streaming guide.

For a quick experiment without writing any page, the online validator and the online converter on this site run the parser in your browser.

FAQ

Why is JSON6 undefined after I load dist/index.min.js?

Because the UMD wrapper publishes the parser as window.JSON5. Add window.JSON6 = window.JSON5 after the script tag, or use lib/json6.js or the ES module build, which do not have this problem.

Why does window.JSON6 stay undefined after loading lib/json6.js?

The file declares a top-level const JSON6. In a classic script that is a global lexical binding, so JSON6.parse() works from other scripts, but it is not a property of window.

Which file is smallest?

dist/index.min.mjs: 16,310 bytes, 5,628 gzipped. The minified UMD file is 32,046 bytes, 11,724 gzipped, because it includes polyfills.

Can I use a CDN?

Public npm mirrors do serve the package files, but I did not test any CDN URL, and I recommend copying the file into your own site. The same file works from your origin without a third-party request.

Does the parser work under a strict Content Security Policy?

Yes. It does not use eval or the Function constructor. I ran it under a policy that allowed only same-origin scripts and inline scripts, with no console messages.

Why not just use JSON.parse in the browser?

JSON.parse rejects comments, unquoted keys, single quotes and trailing commas. JSON6 accepts them, so files written for people can be read directly. For data from servers that you do not control, JSON.parse is still the right tool.

Use JSON6 in the Browser

Pick the ES module build, copy it to your site and start parsing.