Guide for Node.js Developers
Read .json6 files with require(), import, the command line tool and plain fs. Every example here was run on Node 24 with json-6 1.1.7.
TL;DR: Parse files with JSON6.parse(fs.readFileSync(path, 'utf8')) anywhere. To require() a .json6 file run require('json-6/lib/require') once. To import one, start Node with --import json-6/lib/register.mjs (Node 20.6 or newer). To convert files to JSON, run npx json-6 -c file.json6; note the executable is named json-6.
Node Versions and Support
The json-6 package declares engines.node >= 10, and the parser itself only needs let, const and a few string methods, so reading a file with JSON6.parse works on any Node you are likely to run. The loader features need more. The ES module registration file lib/register.mjs calls register() from node:module, which the package README ties to Node 20.6 or newer. On older versions it suggests the --experimental-loader flag.
The release status of Node in October 2026, according to the published release schedule, is: Node 24 is the current Active LTS line until it moves to Maintenance on 20 October 2026; Node 26 was released in May 2026 and is scheduled to enter LTS on 28 October 2026; Node 22 is in Maintenance and reaches end of life in April 2027; and Node 20 reached end of life in April 2026. Check nodejs.org/en/about/previous-releases for the live table. All output on this page comes from Node v24.22.1, and the package was not tested on Node 26 or Node 22 for this page.
| Node line | Status (October 2026) | JSON6 notes |
|---|---|---|
| Node 26 | Current, LTS scheduled for 28 October 2026 | Not tested here. |
| Node 24 | LTS (Maintenance from 20 October 2026) | All examples run here. Runs .ts files by stripping types, see the TypeScript guide. |
| Node 22 | Maintenance LTS, end of life April 2027 | Supports --import and module.register(). Not tested here. |
| Node 20 | End of life since April 2026 | Hooks need 20.6 or newer. Avoid for new work. |
Node will not load .json6 without help. A bare import config from './app.json6' fails with ERR_UNKNOWN_FILE_EXTENSION, and a bare require('./app.json6') executes the file as JavaScript and throws a SyntaxError. Pick one of the methods below.
The Simplest Way: fs + parse
You do not need any hook to use JSON6 in Node. Read the file as a UTF-8 string and hand it to JSON6.parse. This works in CommonJS and ESM, on every Node version, and it keeps the loading step visible in your code, which is the best choice for most applications. Here is the file used for the examples on this page.
// app.json6 - comments, unquoted keys and trailing commas { name: 'demo', port: 0x1F90, // 8080 tags: ['a', 'b',], motd: `Hello world`, }node-fs.mjs
import { readFileSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import JSON6 from 'json-6'; // Synchronous: good for startup configuration const config = JSON6.parse(readFileSync(new URL('./app.json6', import.meta.url), 'utf8')); console.log(config); // Asynchronous const again = JSON6.parse(await readFile(new URL('./app.json6', import.meta.url), 'utf8')); console.log(again.port === config.port);Run it:
node node-fs.mjsOutput:
{ name: 'demo', port: 8080, tags: [ 'a', 'b' ], motd: 'Hello\nworld' } true Always pass 'utf8'. Without an encoding readFileSync returns a Buffer, and the parser expects text. Strip a UTF-8 byte order mark if your editor writes one, because a BOM is not JSON6 whitespace. The code text.replace(/^\uFEFF/, '') does it.
The require() Hook (CommonJS)
The package registers .json6 with Node's CommonJS loader when you load json-6/lib/require. Do it once at program start, before any require of a .json6 file. After that require('./app.json6') works, and so does require('./app'), because Node tries registered extensions after .js and .json. Like all require calls the result is cached, so every caller shares one object.
node-require.cjs
require('json-6/lib/require'); const config = require('./app.json6'); console.log(config); // The extension is optional once the hook is registered. console.log(require('./app') === config);Run it:
node node-require.cjsOutput:
{ name: 'demo', port: 8080, tags: [ 'a', 'b' ], motd: 'Hello\nworld' } false Note the module path: it is json-6/lib/require, not json-6/require and not json-6/lib/register. The older README text and some comments in the source still use other spellings; the path above is the one the package exports map allows. The hook only affects require(). It does not make import work, and it does not apply to code that runs before it is registered.
From an ES module you can still use the hook through createRequire, which is handy when you want a synchronous load inside a mostly-ESM codebase.
node-createrequire.mjs
import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); require('json-6/lib/require'); console.log(require('./app.json6').name);Run it:
node node-createrequire.mjsOutput:
demoimport with --import register.mjs
ES module loading is customized with hooks, and Node only lets you install them before your program runs. The package ships lib/register.mjs for this. Start Node with --import json-6/lib/register.mjs and every import of a path ending in .json6 resolves to a small module that parses the file and exports the result as default. The imported module has a default export only, so you cannot write named imports for the keys in the file.
node-esm.mjs
import config from './app.json6'; console.log(config); // Dynamic import works too, and always gives you { default }. const dynamic = await import('./app.json6'); console.log(dynamic.default.name);Run it:
node --import json-6/lib/register.mjs node-esm.mjsOutput:
{ name: 'demo', port: 8080, tags: [ 'a', 'b' ], motd: 'Hello\nworld' } demo Typing the flag every time gets old. Put it in an npm script, or use NODE_OPTIONS, which Node reads for every process it starts, including child processes. Both forms run the same file below.
node-esm-options.mjs
import config from './app.json6'; console.log(config.name, config.port);Run it:
NODE_OPTIONS="--import json-6/lib/register.mjs" node node-esm-options.mjsOutput:
demo 8080{ "scripts": { "start": "node --import json-6/lib/register.mjs server.mjs" } } The hook reads the file once per import and returns a module whose source is generated code that calls the parser. Edits to the file are not picked up until you restart the process (module cache). Imported data is not frozen, so treat it as read-only by convention. Use the plain import form, which is the one the README documents.
The register flag only exists on the command line or in NODE_OPTIONS. It is not available inside an already running program, and it does not apply to bundlers or to browsers. For those, see the browser and bundler guide.
The Older --experimental-loader Flag
Before module.register() existed, loader hooks were passed as --experimental-loader. The package keeps this path alive: json-6/lib/import.mjs is the same hook module that register.mjs registers. The README mentions it for older Node versions. On the Node 24 used for this page it still works but prints an experimental warning telling you to use register() instead. The output below has those warning lines removed so the result is readable; run it yourself to see them.
node-loader.mjs
import config from './app.json6'; console.log(config.tags);Run it:
node --experimental-loader json-6/lib/import.mjs node-loader.mjsOutput:
[ 'a', 'b' ] Prefer --import json-6/lib/register.mjs whenever your Node supports it. The experimental loader flag may be removed in a future release, as the warning itself says.
The Command Line Tool
The package has a bin entry that points at lib/cli.js. The README says the command is json6, but package.json writes "bin": "lib/cli.js" as a plain string, and npm names a string-form executable after the package. That makes the installed command json-6. In my test node_modules/.bin contained json-6, json-6.cmd and json-6.ps1 and no json6. Run it through npx json-6, an npm script, or node node_modules/json-6/lib/cli.js.
There is exactly one feature: -c compiles each listed file into a sibling JSON file with the same base name. It parses with JSON6.parse and writes JSON.stringify(value, null, 4) plus a newline. The indent is fixed at four spaces and there are no options for output path, stdin or watching (the source marks these as TODO). Any other invocation prints usage and exits with status 1. The project itself uses this to build its own package.json from a hand-written package.json6.
node-cli.mjs
// Compile app.json6 to app.json with the package's command line tool, // then show the file it wrote. import { execFileSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const cli = fileURLToPath(import.meta.resolve('json-6/lib/cli.js')); execFileSync(process.execPath, [cli, '-c', 'app.json6'], { stdio: 'inherit' }); console.log(readFileSync('app.json', 'utf8'));Run it:
node node-cli.mjsOutput:
{ "name": "demo", "port": 8080, "tags": [ "a", "b" ], "motd": "Hello\nworld" } In a project, the same step is one npm script. Put the source file under version control, generate the JSON in a build step, and add the generated file to .gitignore if nothing else reads it directly.
{ "scripts": { "build:config": "json-6 -c app.json6" } }For anything the CLI cannot do (a different indent, reading stdin, converting a whole folder) write a ten-line script. This one takes file names on the command line and prints minified JSON.
node-convert.mjs
import { readFileSync } from 'node:fs'; import JSON6 from 'json-6'; const files = process.argv.slice(2); if (files.length === 0) { console.error('usage: node node-convert.mjs file.json6 [more.json6 ...]'); process.exit(1); } for (const file of files) { const value = JSON6.parse(readFileSync(file, 'utf8')); console.log(file + ' -> ' + JSON.stringify(value)); }Run it:
node node-convert.mjs app.json6Output:
app.json6 -> {"name":"demo","port":8080,"tags":["a","b"],"motd":"Hello\nworld"}The command line tools page covers the CLI together with other converters, and the online converter does the same in a browser.
Config Loading Patterns
A JSON6 config file is pleasant to write but the file is untrusted input: a typo, a missing key or a wrong type should produce a clear error at startup, not a crash an hour later. The pattern that works is read, parse, merge over defaults, validate, freeze. Here it is in about thirty lines, with a clear message for the two failure modes you will actually hit.
node-loadconfig.mjs
import { readFileSync } from 'node:fs'; import JSON6 from 'json-6'; const DEFAULTS = { port: 3000, host: 'localhost', tags: [] }; export function loadConfig(path, env = process.env) { let text; try { text = readFileSync(path, 'utf8'); } catch (err) { if (err.code === 'ENOENT') return Object.freeze({ ...DEFAULTS }); throw err; } let parsed; try { parsed = JSON6.parse(text); } catch (err) { // JSON6 throws plain Error with the position at the end: "... [line:column]" const where = /\[(\d+):(\d+)\]\s*$/.exec(err.message); throw new Error(`Invalid config ${path}` + (where ? ` at line ${where[1]}, column ${where[2]}` : ''), { cause: err }); } if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new TypeError(`Config ${path} must contain an object`); } const merged = { ...DEFAULTS, ...parsed }; if (env.APP_PORT) merged.port = Number(env.APP_PORT); // environment wins if (!Number.isInteger(merged.port) || merged.port < 1 || merged.port > 65535) { throw new RangeError(`Config port must be 1-65535, got ${merged.port}`); } return Object.freeze(merged); } console.log(loadConfig('app.json6')); console.log(loadConfig('missing.json6')); console.log(loadConfig('app.json6', { APP_PORT: '9000' }).port); for (const bad of ['bad-syntax.json6', 'bad-type.json6']) { try { loadConfig(bad); } catch (err) { console.log(err.constructor.name + ': ' + err.message); } }Run it:
node node-loadconfig.mjsOutput:
{ port: 8080, host: 'localhost', tags: [ 'a', 'b' ], name: 'demo', motd: 'Hello\nworld' } { port: 3000, host: 'localhost', tags: [] } 9000 Error: Invalid config bad-syntax.json6 at line 3, column 9 TypeError: Config bad-type.json6 must contain an objectThree details in that listing deserve a comment. The missing-file branch returns the defaults, which is the right behavior for optional config and wrong for required config; decide per file. The environment override runs after parsing so that deployment settings beat the file. And the validation is hand-written; for anything bigger than a few keys use a schema library, as shown in the TypeScript guide.
Finding the config file
Tools often look for a config file in the current directory and then in each parent, stopping at the first match. That is a short loop, and it works with any extension.
node-find-up.mjs
import { existsSync } from 'node:fs'; import { dirname, join, parse } from 'node:path'; export function findUp(name, start = process.cwd()) { let dir = start; const { root } = parse(dir); while (true) { const candidate = join(dir, name); if (existsSync(candidate)) return candidate; if (dir === root) return null; dir = dirname(dir); } } const found = findUp('app.json6'); console.log(found && found.endsWith('app.json6') ? 'found app.json6' : found); console.log(findUp('definitely-not-here.json6'));Run it:
node node-find-up.mjsOutput:
found app.json6 null I am not aware of a config-discovery library that reads .json6 out of the box, and a search of the npm registry for json6 turned up nothing of the kind (the closest names are require-json6, a 2020 require hook, and a placeholder called json6-config with no description). If you use a generic loader library, register a custom loader for the extension that calls JSON6.parse; check that library's documentation, because I did not test any of them.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
ERR_UNKNOWN_FILE_EXTENSION on import | The register hook is not active | Start Node with --import json-6/lib/register.mjs, or read the file with fs. |
SyntaxError pointing into your .json6 file when you require it | The require hook was not loaded first | Run require('json-6/lib/require') before the first require('./x.json6'). |
json6: command not found | The executable is named json-6 | Use npx json-6 -c file.json6. |
catch (e) { if (e instanceof SyntaxError) ... } never fires | The parser throws plain Error | Catch everything around the parse call, or test the message. |
Trailing // comment at end of file with no newline | Accepted silently | Add a final newline, or pass warnWithCommentWithoutEOL: true to see a warning. |
One last note on security. Parsing with JSON6.parse never executes code: it is a hand-written scanner, and searching lib/json6.js finds no call to eval or the Function constructor. Back-tick strings are plain strings, with no template substitution. The risk appears only when you later paste the text into JavaScript. If that is possible, use the esStrictCompatible and forbidTemplateSubstitution options described in the JavaScript guide.
FAQ
What is the difference between require('json-6/lib/require') and --import json-6/lib/register.mjs?
The first teaches the CommonJS require() function about .json6 files and can be called from code. The second installs ES module loader hooks for import and must be given on the command line, or through NODE_OPTIONS, before the program starts.
Why does the README say json6 but my shell only knows json-6?
The package declares its executable as a plain string, so npm names it after the package, json-6. Run npx json-6 -c file.json6. The behavior is the same.
Can I use .json6 files with Node's built-in test runner or watch mode?
They are ordinary imports once the register hook is on, so pass the flag to the runner as node --import json-6/lib/register.mjs --test. I did not test watch mode. Remember the module cache: a changed file needs a restart.
Does Node ever load .json6 natively?
No. Node has no built-in knowledge of the extension, so a bare import or require fails. Use one of the hooks, or read the file with fs.
Is it safe to parse untrusted JSON6?
The parser does not execute input. As with any parser, guard against large inputs and deeply nested documents by checking the size before you parse, and validate the shape of the result before you use it.
Which file extension should I use?
Use .json6. The README says JSON6 content may not work with existing JSON parsers, which is why it has its own extension, and the hooks above key on it.
Use JSON6 in Your Node.js Project
Start with a plain fs read, add a hook only when you need one.