JSON6 API Reference for JavaScript

The complete API reference for the json-6 package: parse, stringify, escape, the streaming parser and every option, tested on json-6 1.1.7.

TL;DR: Install with npm install json-6, then import JSON6 from 'json-6' (ESM) or const JSON6 = require('json-6') (CommonJS). The package exports one object with parse, stringify, escape, begin, stringifier and version. ESM has only a default export, so named imports such as import { parse } fail.

Install and Import

The JavaScript implementation of JSON6 is the json-6 package on npm, written by d3x0r and published under the MIT license. The latest release is 1.1.7. It has no runtime dependencies and declares engines.node >= 10. The package name is json-6 with a hyphen; the format and the global object are spelled JSON6.

npm install json-6

The package ships three flavors of the same parser, and the exports map in its package.json picks one for you. Under Node, require() loads lib/json6.js and import loads a thin ES module wrapper that re-exports the same object as its default export. Bundlers and anything that sets the browser condition get dist/index.js, a UMD build that includes small polyfills. The result is the same API everywhere, which is why this page covers it once.

ES modules

js-import.mjs

import JSON6 from 'json-6'; import * as namespace from 'json-6'; console.log(JSON6.version); console.log(Object.keys(JSON6)); // The ES module wrapper has exactly one export: default. console.log(Object.keys(namespace)); // Named imports do not work, because there is no named export. try { await import('json-6').then((m) => { if (!('parse' in m)) throw new Error('no named export "parse"'); }); } catch (err) { console.log(err.message); }

Run it:

node js-import.mjs

Output:

1.1.7 [ 'escape', 'begin', 'parse', 'stringifierActive', 'stringifier', 'stringify', 'version' ] [ 'default' ] no named export "parse"

Because the wrapper only has a default export, a static import { parse } from 'json-6' is a link-time SyntaxError in Node ("does not provide an export named 'parse'"), and TypeScript rejects it as well. Always import the default and call methods on it, or destructure after importing: const { parse, stringify } = JSON6;. Methods do not depend on this, so destructuring is safe.

CommonJS

js-require.cjs

const JSON6 = require('json-6'); const settings = JSON6.parse(`{ // trailing comma and unquoted keys are fine retries: 3, endpoints: ['a', 'b',], }`); console.log(settings); console.log(JSON6.stringify(settings));

Run it:

node js-require.cjs

Output:

{ retries: 3, endpoints: [ 'a', 'b' ] } {endpoints:["a","b"],retries:3}

Add the package to your project and keep the spelling straight: the npm package is json-6, the object is JSON6, the file extension is .json6, and the command line tool is json-6 (see the Node.js guide for why).

JSON6.parse(text, reviver?, options?)

parse reads one JSON6 value from a string and returns the JavaScript value. The signature is parse(text[, reviver][, options]). The declared type of text is unknown but you should pass a string. The return type is unknown, which is deliberate: the parser cannot know your shape, so you narrow it yourself (the TypeScript guide shows how).

The accepted grammar is everything JSON5 accepts plus the JSON6 additions: back-tick strings, multi-line strings in every quote style, \u{...} escapes, underscore separators, 0b and 0o numbers, leading-zero decimals, repeated signs, the undefined keyword, empty array members and extra whitespace characters. The next example packs most of it into one document.

js-parse.mjs

import JSON6 from 'json-6'; const value = JSON6.parse(`{ // comments are allowed name: 'demo', // unquoted key, single quotes motd: \`back ticks and a real newline\`, letter: '\\u{41}', hex: 0xFF, binary: 0b0110_1001, // underscores are ignored octal: 0o17, padded: 0123, // decimal 123, not octal signs: ---5, nothing: undefined, holes: [1,,3], half: .5, big: Infinity, last: 'trailing comma next', }`); console.log(value); console.log(JSON6.parse('undefined'), JSON6.parse('[]'), JSON6.parse('"just a string"')); console.log('code point:', JSON6.parse("'\\u{1F600}'").codePointAt(0).toString(16));

Run it:

node js-parse.mjs

Output:

{ name: 'demo', motd: 'back ticks\nand a real newline', letter: 'A', hex: 255, binary: 105, octal: 15, padded: 123, signs: -5, nothing: undefined, holes: [ 1, <1 empty item>, 3 ], half: 0.5, big: Infinity, last: 'trailing comma next' } undefined [] just a string code point: 1f600

Three details are worth knowing. First, a document must contain exactly one top-level value for parse: an empty string, whitespace or comments alone throw "No value found in document", and two values side by side throw "Two values with no separator between them". If your input contains many values, use the streaming parser. Second, parse('undefined') is legal and returns undefined. Third, array holes come back as real holes (sparse array slots), which print as <1 empty item> in Node; forEach and map skip them, but for...of yields undefined.

The package README states that JSON6 "works with all existing JSON content". The check below parses a real JSON document with both parsers and asserts that they return the same value.

js-json-compat.mjs

import JSON6 from 'json-6'; import assert from 'node:assert/strict'; const jsonText = JSON.stringify({ id: 7, tags: ['a', 'b'], nested: { ok: true, none: null, ratio: 0.25, exp: 1e21, text: 'line\nbreak "quoted"' }, }); assert.deepEqual(JSON6.parse(jsonText), JSON.parse(jsonText)); console.log('JSON6.parse matches JSON.parse for:', jsonText);

Run it:

node js-json-compat.mjs

Output:

JSON6.parse matches JSON.parse for: {"id":7,"tags":["a","b"],"nested":{"ok":true,"none":null,"ratio":0.25,"exp":1e+21,"text":"line\nbreak \"quoted\""}}

The Reviver Function

The second argument is an optional reviver with the same shape as the one in JSON.parse: (key, value) => newValue. It is called bottom-up, children before parents, and finally once for the root with the empty key "". Inside the function this is the object that holds the key. Returning undefined from the reviver deletes an object property, or leaves a hole in an array.

Since 1.1.7 the value the reviver returns for the root is used. Earlier versions discarded it, so parse("2", v => v * 10) returned 2; that was fixed in release 1.1.7 (issue 61). Note that the reviver receives the key first, so the one-argument shorthand in the changelog v => v * 10 actually multiplies the empty-string key and gives 0. Write (key, value) and test for the root key, as the fourth case below does.

js-reviver.mjs

import JSON6 from 'json-6'; // 1. Call order: children first, then the root with the key "". const order = []; JSON6.parse("{a: 1, b: {c: [10, 20]}, d: 'x'}", (key, value) => { order.push(JSON.stringify(key)); return value; }); console.log(order.join(' ')); // 2. Revive dates. const withDate = JSON6.parse("{ created: '2026-10-27T00:00:00Z', n: 1 }", (key, value) => key === 'created' ? new Date(value) : value, ); console.log(withDate.created instanceof Date, withDate.created.toISOString()); // 3. Returning undefined drops a property. console.log(JSON6.parse('{a: 1, secret: 2, c: 3}', (key, value) => (key === 'secret' ? undefined : value))); // 4. The root value is revived too. console.log(JSON6.parse('2', (key, value) => (key === '' ? value * 10 : value)));

Run it:

node js-reviver.mjs

Output:

"a" "0" "1" "c" "b" "d" "" true 2026-10-27T00:00:00.000Z { a: 1, c: 3 } 20

Parse Options

The third argument is an options object. It is shared with JSON6.begin() and reset() for streaming. Every option is off by default, so out of the box the parser accepts the full historical JSON6 grammar. Pass null or undefined as the reviver if you only want to set options.

OptionDefaultWhat it does
esStrictCompatiblefalseNarrows the grammar to a subset of ECMAScript strict mode. Rejects leading-zero numbers like 0123, legacy octal string escapes ("\012"), literal line breaks inside ' and " strings, back-tick object keys, unquoted keys that are not identifiers or numbers ({a-b: 1}), and two or more consecutive signs (--5).
forbidTemplateSubstitutionfalseRejects an unescaped ${ inside a back-tick string, so a value can never turn into a template-literal substitution if the text is later evaluated as JavaScript. Independent of esStrictCompatible.
normalizeNumericKeysfalseReplaces an unquoted numeric key with String(Number(key)), so {1e3: true} gives the key "1000" rather than the source text "1e3". Added in 1.1.6.
warnWithCommentWithoutEOLfalseLogs a console warning when a // comment runs to the end of the document with no line break. The input is accepted either way. Warnings are off by default since 1.1.6.

js-options.mjs

import JSON6 from 'json-6'; function attempt(label, fn) { try { console.log(label, '->', fn()); } catch (err) { console.log(label, '-> throws:', err.message.replace(/ '.' unexpected.*$/s, '')); } } const loose = "{'a-b': 0123}"; attempt('default ', () => JSON6.parse(loose)); attempt('esStrictCompatible ', () => JSON6.parse(loose, null, { esStrictCompatible: true })); const tpl = '`total: ${x}`'; attempt('default ', () => JSON6.parse(tpl)); attempt('forbidTemplate... ', () => JSON6.parse(tpl, null, { forbidTemplateSubstitution: true })); const numKey = '{1e3: true}'; attempt('default ', () => JSON6.parse(numKey)); attempt('normalizeNumericKeys', () => JSON6.parse(numKey, null, { normalizeNumericKeys: true }));

Run it:

node js-options.mjs

Output:

default -> { 'a-b': 123 } esStrictCompatible -> throws: Legacy octal / leading-zero numbers are not allowed when the 'esStrictCompatible' option is set default -> total: ${x} forbidTemplate... -> throws: Template substitution ${ } is not allowed when the 'forbidTemplateSubstitution' option is set default -> { '1e3': true } normalizeNumericKeys -> { '1000': true }

esStrictCompatible makes JSON6 stricter than its default, not stricter than JSON5. The exact promise is "if it parses, you can paste it into a JavaScript module". If your files are only ever read by a JSON6 parser, leave the options off; they exist for files that must also be valid JavaScript. See JSON6 vs JSON5 for the grammar differences.

Errors and Messages

The parser throws plain Error objects, not SyntaxError. This surprises people coming from JSON.parse and from JSON5, so do not write err instanceof SyntaxError to detect bad input; it is never true here. The message names the problem, the offending character, a short snippet of nearby text, and finishes with a [line:column] pair. There is no separate lineNumber property to read. Since 1.1.6 the columns are correct, counting underscores in numbers and line breaks inside comments and strings.

js-errors.mjs

import JSON6 from 'json-6'; const inputs = [ '{a:1,,b:2}', // doubled comma in an object '', // nothing at all '[1 2]', // two values, no separator '{a}', // field name with no value '{ a: 1,\n b: @ }', // unexpected character on line 2 '[tru]', // incomplete keyword '1e', // malformed number ]; for (const text of inputs) { try { JSON6.parse(text); console.log('parsed', JSON.stringify(text)); } catch (err) { console.log(JSON.stringify(text).padEnd(18), err.constructor.name + ':', err.message); } }

Run it:

node js-errors.mjs

Output:

"{a:1,,b:2}" Error: bad context; excessive commas while parsing; ',' unexpected at 6 (near ':1,[,]b:2}') [1:6] "" Error: No value found in document "[1 2]" Error: Two values with no separator between them '2' unexpected at 4 (near '[1 [2]]') [1:4] "{a}" Error: Object field name with no value '}' unexpected at 3 (near '{a[}]') [1:3] "{ a: 1,\n b: @ }" Error: fault parsing '@' unexpected at 13 (near 'b: [@] }') [2:5] "[tru]" Error: Incomplete keyword ']' unexpected at 5 (near 'tru[]]') [1:5] "1e" Error: Invalid number '1e' at 2 [1:2]

For user-facing diagnostics, pull the location out of the message with a regular expression such as /\[(\d+):(\d+)\]\s*$/. The parse error guide walks through the common messages, and the error reference lists them.

JSON6.stringify(value, replacer?, space?, options?)

stringify serializes a value to JSON6 text. The signature is stringify(value[, replacer][, space][, options]). The options object is the fourth parameter, after space, which is different from JSON5's stringify. Its only option is sortKeys.

Know what the output looks like before you rely on it. Object keys are written bare when they are valid identifiers and quoted otherwise; strings are always written with double quotes; and there are no trailing commas and no comments. Keys are sorted by default (sortKeys: true), which makes output stable but reorders your objects. Pass { sortKeys: false } to keep each object's own key order. The output of stringify is meant for JSON6.parse, not for JSON.parse, because it can contain unquoted keys, \' escapes, literal line breaks and bare Infinity.

js-stringify.mjs

import JSON6 from 'json-6'; const value = { zeta: 1, alpha: [1, 2, { deep: true }], 'needs-quotes': "it's", empty: {}, list: [] }; console.log('compact: ', JSON6.stringify(value)); console.log('keep order: ', JSON6.stringify(value, null, null, { sortKeys: false })); console.log('two spaces:'); console.log(JSON6.stringify(value, null, 2)); console.log('tab:'); console.log(JSON6.stringify({ a: 1, b: [true] }, null, '\t')); // Numbers, keywords and holes console.log(JSON6.stringify({ n: -0, inf: Infinity, nan: NaN, u: undefined, arr: [1, , 3, undefined] }));

Run it:

node js-stringify.mjs

Output:

compact: {alpha:[1,2,{deep:true}],empty:{},list:[],"needs-quotes":"it\'s",zeta:1} keep order: {zeta:1,alpha:[1,2,{deep:true}],"needs-quotes":"it\'s",empty:{},list:[]} two spaces: { alpha: [ 1, 2, { deep: true } ], empty: {}, list: [], "needs-quotes": "it\'s", zeta: 1 } tab: { a: 1, b: [ true ] } {arr:[1,,3,undefined],inf:Infinity,n:0,nan:NaN}

The space argument works like the native one: a number means that many spaces, a string is used as the indent. One difference is that the number is not clamped to ten. JSON.stringify caps the indent at ten characters, while JSON6 honors the number you pass. Arrays and objects print one element per line when indented, including short arrays, and an empty array or object prints as [] or {}. Hole positions survive: [1,,3] stays [1,,3], and a trailing hole keeps its comma so it survives a round trip (fixed in 1.1.6). In objects, properties whose value is undefined, a function or a symbol are omitted, as they are with JSON.stringify; inside arrays those values print as the keyword undefined.

Replacers and Key Order

The second argument filters or transforms what gets written. It can be a function (key, value) => value or an array of keys to keep. A function is called first with the empty key for the root, then for every property, and it sees raw values: since 1.1.7 a replacer receives the unquoted string and the library quotes the result afterwards (earlier versions passed strings already quoted, and a string the replacer returned was emitted bare, issue 62). Returning undefined drops the property. An array replacer keeps only the listed keys, at every depth, and the output is still sorted unless you disable that.

js-replacer.mjs

import JSON6 from 'json-6'; const data = { id: 7, password: 'hunter2', nested: { id: 8, password: 'x', note: 'ok' } }; // Function replacer: drop secrets, transform numbers console.log(JSON6.stringify(data, (key, value) => { if (key === 'password') return undefined; if (typeof value === 'number') return value * 10; return value; })); // Array replacer: allow-list of keys, applied at every depth console.log(JSON6.stringify(data, ['id', 'nested', 'note'])); // The replacer sees the root first, with the key '' const seen = []; JSON6.stringify({ a: [1] }, (key, value) => { seen.push(key === '' ? '(root)' : key); return value; }); console.log(seen.join(' '));

Run it:

node js-replacer.mjs

Output:

{id:70,nested:{id:80,note:"ok"}} {id:7,nested:{id:8,note:"ok"}} (root) a 0

What stringify Does Not Do

JSON6.stringify is a small serializer for plain data, not a drop-in for JSON.stringify. These behaviors were checked against 1.1.7 and are the ones most likely to bite you.

InputResultNotes
new Date(){}No toJSON support. Convert dates to strings yourself, or use a replacer.
Object with a toJSON() methodMethod ignoredOwn enumerable properties are written instead.
BigInt{}No error and no value. Convert with String() in a replacer.
Map, Set{}Convert to arrays or objects first.
Circular referenceRangeError"Maximum call stack size exceeded". There is no cycle detection.
NaN, Infinity, -0NaN, Infinity, 0Written as bare keywords; -0 loses its sign.
String with a line breakRaw line breakNot escaped, which JSON6.parse reads back because strings may span lines.
Object key orderSortedDisable with { sortKeys: false }.

js-limits.mjs

import JSON6 from 'json-6'; console.log('Date: ', JSON6.stringify({ when: new Date(0) })); console.log('toJSON: ', JSON6.stringify({ toJSON() { return { x: 1 }; }, y: 2 })); console.log('BigInt: ', JSON6.stringify({ n: 10n })); console.log('Map: ', JSON6.stringify(new Map([[1, 2]]))); const loop = {}; loop.self = loop; try { JSON6.stringify(loop); } catch (err) { console.log('Cycle: ', err.constructor.name, '-', err.message); } // Workaround for dates and BigInt: convert them in a replacer. const safe = JSON6.stringify( { when: new Date(0), n: 10n, ok: 1 }, (key, value) => { if (value instanceof Date) return value.toISOString(); if (typeof value === 'bigint') return value.toString(); return value; }, ); console.log('Fixed: ', safe); // Output is for JSON6.parse, not JSON.parse const text = JSON6.stringify({ s: "it's", k: 1 }); console.log(text); console.log(JSON6.parse(text)); try { JSON.parse(text); } catch (err) { console.log('JSON.parse:', err.constructor.name); }

Run it:

node js-limits.mjs

Output:

Date: {when:{}} toJSON: {y:2} BigInt: {} Map: {} Cycle: RangeError - Maximum call stack size exceeded Fixed: {n:"10",ok:1,when:"1970-01-01T00:00:00.000Z"} {k:1,s:"it\'s"} { k: 1, s: "it's" } JSON.parse: SyntaxError

If you need JSON that other tools can read, use JSON.stringify on the parsed value instead. The migration guide and the online converter show the JSON6 to JSON direction.

escape, stringifier and version

JSON6.escape(string) puts a backslash in front of the double quote, single quote, back-tick and backslash characters. The README describes the purpose as preventing "JSON injection" when you build JSON6 text by string concatenation. It does not add surrounding quotes and it does not touch line breaks or tabs. Building text from parts is discouraged, but when you must, escape each string yourself before you place it between quotes.

JSON6.stringifier() returns a reusable serializer object with the methods and properties that stringify uses internally: stringify(value, replacer, space), setQuote(char), sortKeys and ignoreNonEnumerable. setQuote changes the quote used for string values; with the single quote or back-tick selected, string values use that character instead of the double quote. JSON6.version is the package version string, stamped into the source at build time so it cannot drift from package.json (changed in 1.1.5).

js-helpers.mjs

import JSON6 from 'json-6'; console.log('version:', JSON6.version); console.log('escape: ', JSON6.escape('say "hi" and \'bye\' \\ `tick`')); // Build a document by hand, escaping the untrusted piece. const untrusted = 'x", admin: true, "y": "'; const text = '{ name: "' + JSON6.escape(untrusted) + '" }'; console.log(text); console.log(JSON6.parse(text)); // A reusable serializer with its own settings const serializer = JSON6.stringifier(); serializer.sortKeys = false; console.log(Object.keys(serializer)); console.log(serializer.stringify({ z: 1, a: 2 })); serializer.setQuote("'"); console.log(serializer.stringify({ greeting: 'hello' }));

Run it:

node js-helpers.mjs

Output:

version: 1.1.7 escape: say \"hi\" and \'bye\' \\ \`tick\` { name: "x\", admin: true, \"y\": \"" } { name: 'x", admin: true, "y": "' } [ 'stringify', 'setQuote', 'ignoreNonEnumerable', 'sortKeys' ] {z:1,a:2} {greeting:'hello'}

The serializer returned by stringifier() keeps its settings between calls, which is useful when you serialize many records with the same options, but a fresh object per task is simpler and safe. The declaration files also list JSON6.stringifierActive, an internal slot; do not use it.

Quick Reference

MemberSignatureReturns
JSON6.parse(text, reviver?, options?)The parsed value (unknown). Throws Error on bad input.
JSON6.stringify(value, replacer?, space?, options?)JSON6 text, or undefined for undefined input.
JSON6.escape(string)The string with ", ', back-tick and backslash escaped.
JSON6.begin(callback, reviver?, options?)A stream parser with write, value, reset. See streaming.
JSON6.stringifier()A reusable serializer object.
JSON6.versionstring"1.1.7" for the release tested here.

Compared with the native JSON object, the differences to remember are: errors are plain Error instances, the parser accepts a much larger grammar, stringify sorts keys and does not call toJSON, and the options object comes last. Everything else behaves as you would expect from JSON.parse and JSON.stringify.

You can find the project at github.com/d3x0r/JSON6 and the package at npmjs.com/package/json-6. Use these pages to learn more: the specification for the syntax, the cheat sheet for a one-page summary, and the learning path for a guided order.

FAQ

Why does import { parse } from 'json-6' fail?

The ES module entry exports a single default value. There is no named export called parse. Use import JSON6 from 'json-6' and call JSON6.parse(), or destructure from the default after importing.

Is JSON6.parse a drop-in replacement for JSON.parse?

For valid JSON input, yes: it returns the same result. It also accepts far more syntax, so it will not reject text that JSON.parse rejects. The error type differs, because JSON6 throws plain Error rather than SyntaxError.

Why did stringify reorder my keys?

Because sortKeys defaults to true. Pass { sortKeys: false } as the fourth argument, or set sortKeys = false on a JSON6.stringifier() object, to keep each object's own order.

Can stringify write comments or trailing commas?

No. It never emits comments, trailing commas, back-tick keys or multi-line formatting of strings. It writes compact, valid JSON6 that JSON6.parse can read back.

Does JSON6 work with Date and BigInt?

The parser never produces them, because JSON6 adds no new data types. stringify writes {} for both, so convert them to strings in a replacer. If you need real Date and BigInt values in the format itself, look at JSOX, the related format by the same author.

How big is the package?

In the tested 1.1.7 release the Node source file lib/json6.js is about 61 KB unminified and the browser bundle dist/index.min.js is about 31 KB (about 11.5 KB gzipped); the ES module bundle dist/index.min.mjs is about 16 KB (about 5.5 KB gzipped).

Ready to Use JSON6?

Try the examples in the browser or walk the full learning path.