JSON6 Migrate from JSON5 to JSON6

What stays the same, what is new, and what silently changes meaning when you swap the parser.

Moving boxes carried from a blue house to an emerald house, a magnifier revealing one subtly changed box

Short version: JSON6 parses JSON5 to the same value, with two exceptions we found: \u escapes in bare keys and the __proto__ key. The other risks are around parsing: trailing text is ignored, stringify output differs, and errors are plain Error objects.

The Short Version

JSON6 reads JSON5. Its README says it "includes all features of JSON5 plus" its own additions, and a differential test agrees. On 10-27-2026 we generated 300,000 random JSON5 documents using comments, trailing commas, single and double quotes, line continuations, hex numbers, signs, leading and trailing decimal points, Infinity and NaN, keywords as keys, odd whitespace and line terminators. JSON5 (2.2.3) accepted every one, and json-6 1.1.7 returned an identical value for every one. Outside those generated documents we found two inputs where a valid JSON5 document means something different: \u escapes inside bare keys, and the __proto__ key. Both are covered below.

So most files migrate unchanged. The risk is not files that stop parsing; it is the changes in surrounding behavior: JSON6 accepts input JSON5 rejects, stringify produces different text, errors are a different class, and a few details of key handling differ. This page lists each one with output from running both libraries.

Be sure you want to move. JSON5 has a published specification at json5.org and many independent implementations. JSON6 has one maintained parser, by its author, and almost no tooling that understands the .json6 extension. If the only feature you want is something JSON5 already has, there is little reason to switch.

What JSON6 Adds

Every row in this table is rejected by JSON5 and accepted by JSON6. Both result columns are real output from the two libraries.

Syntax JSON5 2.2.3 JSON6 1.1.7
0b101, 0o17 error: invalid character 'b' at 1:3 [ 5, 15 ]
1_000_000 error: invalid character '_' at 1:2 1000000
[1,,3] (empty member) error: invalid character ',' at 1:4 [ 1, <1 empty item>, 3 ]
back-tick string error: invalid character '`' at 1:1 'text'
quoted string with a raw line break error: invalid character '\n' at 2:0 'a\nb'
\u{41} escape error: invalid character '{' at 1:4 'A'
undefined error: invalid character 'u' at 1:1 undefined
0123 (leading zero) error: invalid character '1' at 1:2 123
--5 error: invalid character '-' at 1:2 5
{ a-b: 1 } (hyphen in bare key) error: invalid character '-' at 1:4 { 'a-b': 1 }
{ 1e3: 1 } (number as bare key) error: invalid character '1' at 1:3 { '1e3': 1 }
"\012" (legacy octal escape) error: invalid character '1' at 1:4 '\x0012'

Also new, and not shown above: underscores work in hexadecimal too (0x_ff parses to 255), and object keys may be back-tick quoted. Every addition is an acceptance change, so none of them can alter a document that already parsed under JSON5. They matter in the other direction: a file written with them will not load in a JSON5 parser. The JSON5 differences page has the full list.

Swapping the API

The parse call is a drop-in: JSON6.parse(text, reviver) behaves like JSON5.parse(text, reviver), with an optional third argument for options. The stringify call is where code needs attention.

JSON5

const JSON5 = require('json5'); const config = JSON5.parse(text); const out = JSON5.stringify(config, { space: 2, quote: '"' });

JSON6

const JSON6 = require('json-6'); const config = JSON6.parse(text); const out = JSON6.stringify(config, null, 2);
Concern JSON5 JSON6
Parse signature parse(text, reviver) parse(text, reviver, options)
Stringify signature stringify(value, replacer, space) or stringify(value, { replacer, space, quote }) stringify(value, replacer, space, options). An options object as the second argument throws "unknown replacer type".
Quote style Chooses quotes, configurable with quote Always double quotes
Key order Insertion order Sorted alphabetically unless { sortKeys: false }
Trailing commas in output Added when space is set Never
Streaming None JSON6.begin() with write()
Node require hook require('json5/lib/register') require('json-6/lib/require')
CLI json5 -c json-6 -c

Changes in Meaning That Do Not Raise an Error

These are the cases to read twice. Each script below ran the same text through both parsers.

const JSON5 = require('json5'); const JSON6 = require('json-6'); function show(label, text) { const run = (parser) => { try { return JSON.stringify(parser.parse(text)); } catch (err) { return 'throws: ' + err.message.split('\n')[0].replace(/^JSON5: /, ''); } }; console.log(label); console.log(' JSON5:', run(JSON5)); console.log(' JSON6:', run(JSON6)); } show('escape in a bare key', '{ \\u0061b: 1 }'); show('text after the value', '{ a: 1 } oops'); show('premature closing brace', '{ a: 1 }, b: 2 }'); show('__proto__ key', '{ "__proto__": { admin: true }, name: "x" }'); show('leading zero', '{ port: 0123 }');
escape in a bare key JSON5: {"ab":1} JSON6: {"\\u0061b":1} text after the value JSON5: throws: invalid character 'o' at 1:10 JSON6: {"a":1} premature closing brace JSON5: throws: invalid character ',' at 1:9 JSON6: {"a":1} __proto__ key JSON5: {"__proto__":{"admin":true},"name":"x"} JSON6: {"name":"x"} leading zero JSON5: throws: invalid character '1' at 1:10 JSON6: {"port":123}

1. Unicode escapes in bare keys

JSON5 decodes \u0061b in an unquoted key to ab. JSON6 keeps the backslash text, so the key becomes the six characters \u0061b. A quoted key ("\u0061b") is decoded correctly by both. This is one of two places we found where valid JSON5 means something different (the other is __proto__, below). The fix is to quote any key that contains an escape, or to write the character itself.

2. Anything after the first value is ignored

JSON5 requires the whole input to be one value. JSON6.parse stops after the first one, so a stray brace, a merge-conflict marker after the closing brace, or two documents joined together loads without complaint. If your JSON5 files relied on the parser to reject malformed input, add the single-value check from the config loader tutorial.

3. The __proto__ key

JSON5 creates an ordinary property. JSON6 assigns it:

const JSON5 = require('json5'); const JSON6 = require('json-6'); const text = '{ "__proto__": { admin: true }, name: "x" }'; const a = JSON5.parse(text); const b = JSON6.parse(text); console.log('JSON5: own keys', Object.keys(a), 'admin =', a.admin); console.log('JSON6: own keys', Object.keys(b), 'admin =', b.admin);
JSON5: own keys [ '__proto__', 'name' ] admin = undefined JSON6: own keys [ 'name' ] admin = true

The inherited property is invisible to Object.keys and JSON.stringify but visible to code that reads config.admin. Global Object.prototype is not modified, so this is not classic prototype pollution, but it is a reason not to feed untrusted input to JSON6.

4. Leading zeros

JSON5 refuses 0123. JSON6 reads it as 123. If the same file is ever evaluated as JavaScript in sloppy mode, those digits mean octal 83. The esStrictCompatible option makes json-6 reject them again, and is a good default for hand-edited config.

5. Typos that used to fail now parse

A bare key may contain hyphens, dots and most punctuation, so { my-key: 1 } and { a.b: 1 } are valid and create keys named my-key and a.b. In JSON5 the same text was an error that caught real typos. Likewise a doubled comma inside an array is no longer an error; it creates a hole.

stringify Is Not the Same Function

JSON5.stringify and JSON6.stringify both emit unquoted keys where possible, but they diverge on almost everything else:

const JSON5 = require('json5'); const JSON6 = require('json-6'); const value = { name: 'app', 'x-y': 1, list: [1, 2], when: new Date(0), skip: undefined, text: 'a\nb', n: NaN, }; console.log(JSON5.stringify(value)); console.log(JSON6.stringify(value, null, 0, { sortKeys: false })); console.log(JSON6.stringify([undefined, 1]), JSON5.stringify([undefined, 1]));
{name:'app','x-y':1,list:[1,2],when:'1970-01-01T00:00:00.000Z',text:'a\nb',n:NaN} {name:"app","x-y":1,list:[1,2],when:{},text:"a b",n:NaN} [undefined,1] [null,1]
  • Dates. JSON5 honors toJSON, so a Date becomes its ISO string. JSON6.stringify does not call toJSON, so the same value becomes {}. Convert dates yourself before stringifying.
  • Arrays with undefined. JSON5 writes null, like JSON. JSON6 writes the word undefined, which only a JSON6 parser can read back.
  • Raw line breaks. JSON6.stringify writes a newline inside a string as an actual newline, not as \n. The result parses with json-6 but not with JSON5 or JSON.
  • Key order. JSON6 sorts by default, which is stable for diffs but changes the order of anything you edit and write back. Pass { sortKeys: false } as the fourth argument to keep insertion order.

If anything other than json-6 reads the output, produce it with JSON.stringify or JSON5.stringify instead and keep JSON6 for reading.

Error Handling

JSON5 throws a SyntaxError with lineNumber and columnNumber properties. json-6 throws a plain Error with neither; the position is only in the message:

const JSON5 = require('json5'); const JSON6 = require('json-6'); for (const parser of [JSON5, JSON6]) { try { parser.parse('{ a: 1 b: 2 }'); } catch (err) { console.log(err.name, '|', err.lineNumber, err.columnNumber, '|', err.message.split('\n')[0]); } }
SyntaxError | 1 8 | JSON5: invalid character 'b' at 1:8 Error | undefined undefined | fault parsing 'b' unexpected at 8 (near ' 1 [b]: 2 }') [1:8]

Code that reads err.lineNumber or filters with instanceof SyntaxError needs to parse the trailing [line:col] instead. The pattern is /\[(\d+):(\d+)\]\s*$/. A few messages carry no position at all, notably "No value found in document", which json-6 reports for an unclosed object or array. See the error reference for every message.

A Compatibility Checker for Your Own Files

Before switching, run every JSON5 file in your repository through both parsers and look for anything that is not "same value". This script does it, and the samples show each outcome:

// compat-check.js - usage: node compat-check.js samples/*.json5 const fs = require('node:fs'); const assert = require('node:assert/strict'); const JSON5 = require('json5'); const JSON6 = require('json-6'); function attempt(parser, text) { try { return { value: parser.parse(text) }; } catch (err) { return { error: err.message.split('\n')[0] }; } } for (const file of process.argv.slice(2)) { const text = fs.readFileSync(file, 'utf8'); const a = attempt(JSON5, text); const b = attempt(JSON6, text); let verdict; if (a.error && b.error) verdict = 'both reject'; else if (a.error) verdict = 'JSON6 accepts what JSON5 rejects'; else if (b.error) verdict = 'JSON6 REJECTS what JSON5 accepts: ' + b.error; else { try { assert.deepStrictEqual(b.value, a.value); verdict = 'same value'; } catch { verdict = 'DIFFERENT VALUE'; } } console.log(file.padEnd(24), verdict); }
node compat-check.js samples/*.json5
samples/plain.json5 same value samples/escaped-key.json5 DIFFERENT VALUE samples/stray-brace.json5 JSON6 accepts what JSON5 rejects samples/with-proto.json5 DIFFERENT VALUE samples/uses-json6.json5 JSON6 accepts what JSON5 rejects

Add it to CI during the migration. After the switch it is also a cheap way to keep a JSON5 escape hatch: any file that reports "same value" will load in either library.

Conversion Checklist

  1. Install json-6 (note the hyphen) and keep json5 installed until the checker reports no surprises.
  2. Run the compatibility checker over every .json5 file. Fix anything reported as "DIFFERENT VALUE", starting with escapes in bare keys.
  3. Search the code for __proto__ in config and for any place JSON5 input comes from users. Keep those on JSON5 or JSON.parse.
  4. Replace JSON5.parse with JSON6.parse. Decide whether to pass { esStrictCompatible: true }.
  5. Replace JSON5.stringify calls. Convert dates first, pass { sortKeys: false } if order matters, and use JSON.stringify for output that other software reads.
  6. Update error handling: no SyntaxError check, no lineNumber.
  7. Add the single-value check wherever a truncated or concatenated file would be harmful.
  8. Rename files from .json5 to .json6 and update the require hook or loader.
  9. Update editor settings. Editors associate .json5 with a language mode; .json6 has none by default, so map it to JSON5 or JavaScript highlighting (see the editors page).
  10. Tell the team which JSON6 features are allowed, because JSON5 tools will not read files that use them.

Going Back to JSON5

There is no converter that rewrites JSON6 into JSON5. Moving back means parsing with json-6, then writing out with JSON5.stringify, which loses comments. If you want to keep the exit open, restrict the files to the JSON5 subset and let the compatibility checker enforce it in CI.

Be careful with esStrictCompatible here. It narrows json-6 toward JavaScript, not toward JSON5: it still accepts 0b and 0o numbers, underscores, back-tick strings, empty array members and undefined, none of which JSON5 allows. Only the checker proves a file is portable.

FAQ

Can I rename my .json5 files to .json6 without changes?

Almost always. We found no differences in 300,000 generated JSON5 documents, and two known exceptions: \u escapes inside bare keys and the __proto__ key. Run the compatibility checker over your own files to be sure.

Does JSON6 replace JSON5?

No. They are independent projects. JSON5 has a published spec and many implementations; JSON6 is one author's extension with one maintained parser, json-6.

Why did my code that catches SyntaxError stop working?

json-6 throws plain Error objects, not SyntaxError. Catch all errors around the parse call, and read the position from the end of the message.

Is JSON6.stringify a drop-in for JSON5.stringify?

No. It sorts keys by default, ignores toJSON, writes undefined in arrays and emits raw newlines inside strings. Check the stringify section before swapping it.

Check Your Files First

Run the compatibility checker on your JSON5 files, then read the config loader tutorial for a strict way to load them.