JSON6 Migrate from JSON to JSON6

Rename the files that benefit, prove the parsers agree, and keep a generated JSON copy for tools that need one.

Data carts crossing a guarded bridge from a gray island to an emerald island

Short version: Valid JSON always parses as JSON6 to the same value, except for a __proto__ key. Rename hand-edited config files to .json6, switch their readers to json-6, and generate .json copies with json-6 -c for tools that need them.

Why Migrating from JSON Is Low Risk

JSON6 is built on the idea that existing JSON keeps working. Every document the standard JSON.parse accepts is also accepted by JSON6.parse and produces the same value. That is a claim worth checking rather than trusting, so we checked it. On 10-27-2026 we generated 150,000 random valid JSON documents (nested objects and arrays, awkward strings with control characters and line separators, large and tiny numbers, keys such as constructor and toString, surrounding whitespace) and compared the two parsers with a deep-equality assertion. Every document that differed contained a "__proto__" key, which is covered under pitfalls below. The other 143,000 or so matched exactly.

So the migration is mostly mechanical. The work is deciding which files benefit, renaming them, switching the code that reads them, and keeping a JSON copy for any tool that cannot read .json6. The payoff is that those files can then carry comments and trailing commas, which produces cleaner diffs and lets the file explain itself.

The Plan

  1. Inventory. List the .json files that people edit by hand. Skip generated files and anything another program writes.
  2. Classify. For each file decide: read only by your own code (migrate), read by a third-party tool (migrate with a generated JSON copy, or leave alone), or part of a public protocol (leave alone).
  3. Verify and rename. Run the script below, which refuses to touch a file the two parsers read differently.
  4. Swap the loader. Replace JSON.parse or require('./x.json') with json-6 for those files.
  5. Generate JSON where needed with json-6 -c and add a test that fails if the copy is stale.
  6. Start using the features. Add comments first. Everything else is optional.

Which Files to Migrate

JSON6 is a good fit for files that humans edit and your own code reads: application config, feature flags, build settings, test fixtures with annotations, and seed data. It is a poor fit for three kinds of file.

  • Files with a fixed name that other tools own. package.json must stay JSON for npm. The json-6 project itself keeps package.json6 as the source and generates package.json with json-6 -c, which is the pattern recommended below. Tools that already accept comments in their own config (TypeScript's tsconfig.json, for example) gain nothing from a different extension.
  • Data that crosses a trust boundary. API responses, uploads and anything from users should go through JSON.parse. JSON6 accepts far more than JSON, ignores trailing text and treats __proto__ specially, none of which you want in a security-sensitive parser.
  • Machine-written files. Logs, caches and lock files gain nothing from comments, and JSON6.stringify does not produce JSON.

A Migration Script That Checks Itself

This script renames each file you name, after proving that both parsers agree about its contents, and adds a header comment. The third file below contains a __proto__ key to show what a refusal looks like.

// migrate.js - usage: node migrate.js config/app.json config/db.json const fs = require('node:fs'); const path = require('node:path'); const assert = require('node:assert/strict'); const JSON6 = require('json-6'); for (const file of process.argv.slice(2)) { try { const text = fs.readFileSync(file, 'utf8'); // 1. Both parsers must agree on what the file means. assert.deepStrictEqual(JSON6.parse(text), JSON.parse(text), 'JSON6 reads this file differently'); // 2. Rename, then put a header comment on top. const target = file.replace(/\.json$/i, '.json6'); fs.renameSync(file, target); fs.writeFileSync(target, `// ${path.basename(target)} (migrated from ${path.basename(file)})\n${text}`); console.log('migrated', file, '->', target); } catch (err) { console.log('SKIPPED ', file, '-', err.message.split('\n')[0]); } }
node migrate.js config/app.json config/db.json config/odd.json
migrated config/app.json -> config/app.json6 migrated config/db.json -> config/db.json6 SKIPPED config/odd.json - JSON6 reads this file differently

The renamed file is still byte-for-byte the original JSON apart from the new first line:

// app.json6 (migrated from app.json) { "name": "orders-api", "port": 8080, "retries": 3, "timeoutMs": 30000, "features": { "beta": false }, "allowedOrigins": ["https://app.example.com"] }

If you use git, run git mv and commit the bare rename before editing, so that history follows the file, then commit the comments as a second change.

Update the Code That Reads the File

Anywhere your code parses the file, swap the parser. The reviver argument works the same way, so callers do not change.

Before

const fs = require('node:fs'); const config = JSON.parse(fs.readFileSync('config/app.json', 'utf8'));

After

const fs = require('node:fs'); const JSON6 = require('json-6'); const config = JSON6.parse(fs.readFileSync('config/app.json6', 'utf8'));

If the code uses require('./config/app.json'), either call require('json-6/lib/require') once at startup and require ./config/app.json6, or read and parse the text yourself as above. An ES module import such as import cfg from './app.json' with { type: 'json' } does not work for .json6: use the --import json-6/lib/register.mjs loader, or read the file. Browser code should fetch the file as text (response.text()) and parse it; response.json() uses the native parser and will fail on the first comment.

Remember that errors change shape. The native parser throws SyntaxError; json-6 throws a plain Error whose message ends in [line:col]. Any catch that checks instanceof SyntaxError needs to change. The parse errors guide shows a replacement.

Keep a JSON Copy for Tools That Need It

When a tool insists on .json, keep the JSON6 file as the source of truth and generate the JSON from it. The package ships a command for exactly this: json-6 -c file.json6 writes file.json next to it, indented four spaces.

{ "scripts": { "build:config": "json-6 -c config/app.json6", "test": "node --test" }, "dependencies": { "json-6": "^1.1.7" } }

Treat the generated file as a build artifact. You can commit it, so that tools work from a fresh checkout, or ignore it and generate it in CI; either way, add a test so the two never drift apart. This one was run, first passing, then after editing the source without regenerating:

// test/config-copy.test.js - the generated JSON must match the JSON6 source. const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const JSON6 = require('json-6'); test('app.json is up to date with app.json6', () => { const source = JSON6.parse(fs.readFileSync('config/app.json6', 'utf8')); const generated = JSON.parse(fs.readFileSync('config/app.json', 'utf8')); assert.deepStrictEqual(generated, source, 'run: npm run build:config'); });
ok 1 - app.json is up to date with app.json6 # tests 1 # pass 1 # fail 0
not ok 1 - app.json is up to date with app.json6 error: |- run: npm run build:config # tests 1 # pass 0 # fail 1

The conversion goes through a JavaScript value and then JSON.stringify. Comments disappear (they exist only in the source), Infinity and NaN become null, undefined properties are dropped, and sparse-array holes become null. Numbers written in hex, binary or with separators come out as ordinary decimals.

Adopt Features Gradually

Nothing forces you to rewrite a file in one go. A sensible order, from least to most controversial in code review:

  1. Comments. Explain why a value is what it is, and mark values that must change per environment. This is the feature most people migrate for.
  2. Trailing commas. Adding an entry changes one line in the diff instead of two, and reordering entries stops breaking the file.
  3. Numeric separators and hex. 30_000 reads better than 30000; 0xFF better than 255 for masks.
  4. Bare keys and single quotes. Cosmetic. Pick one style and keep it, because no formatter understands the .json6 extension, so consistency is a convention you enforce in review.

Here is the earlier file after a few minutes of editing:

// app.json6 - orders-api settings { name: 'orders-api', port: 8080, // How many times a failed upstream call is retried. retries: 3, timeoutMs: 30_000, // 30 seconds features: { beta: false, // flip to true on staging only }, allowedOrigins: [ 'https://app.example.com', ], }

The only change that matters to a reader is the explanatory comments. The key point of the gradual approach is that every intermediate state is a valid, working file.

Pitfalls After the Switch

  • Trailing text is ignored. JSON.parse rejects {"a":1} oops. JSON6.parse returns { a: 1 }. A file damaged by a merge conflict or a stray brace can therefore load as a truncated object without any error. Parse the text a second time wrapped in an array with a sentinel, which proves it held exactly one value, as in the config loader.
  • The __proto__ key. JSON.parse creates an ordinary property named __proto__. JSON6.parse assigns it, so an object value changes the prototype of the result (an inherited isAdmin: true becomes visible as result.isAdmin) and a non-object value is silently dropped. This does not pollute Object.prototype globally, but untrusted input should not use JSON6.
  • Truncated files. An unclosed brace reports "No value found in document" rather than naming the problem.
  • Errors are not SyntaxErrors, as above.
  • Round-tripping loses comments. There is no API that edits a file and preserves its comments. Parse-modify-stringify discards them, so programmatic edits to a hand-annotated file need a text-level approach.
  • Large integers behave as in JSON. 9007199254740993 parses to 9007199254740992 in both parsers, and neither supports BigInt.

Migration Checklist

  • The file is hand-edited and read only by code you control.
  • The migration script confirmed identical parsed values.
  • Every reader now calls JSON6.parse (or the require hook), and error handling no longer depends on SyntaxError.
  • Any tool that needs JSON gets a generated copy, with a test that detects drift.
  • Input is parsed with a strict single-value check if it could ever be truncated or concatenated.
  • The team agreed on a style (quotes, bare keys, trailing commas), because nothing enforces it.
  • CI parses every .json6 file, so a syntax error fails the build.

FAQ

Will my existing JSON files break?

No. A JSON document parses to the same value with JSON6.parse. We tested 150,000 generated documents and found a difference only for the __proto__ key. You only need to change the code that parses them.

Can I keep package.json as JSON?

Yes, and you have to: npm reads package.json directly. The json-6 project keeps package.json6 and generates package.json with json-6 -c.

Can I mix JSON and JSON6 in one project?

Yes. Use JSON.parse for files and payloads that must be strict JSON and JSON6.parse for hand-edited .json6 files.

Is there a formatter for .json6 files?

None that we could find. A search of npm on 10-27-2026 returned no JSON6 formatter or linter. Agree on a style and rely on the parser in CI to catch syntax errors.

Start with One File

Pick one hand-edited config, run the migration script on it and add a comment explaining its least obvious value.