Parse and stringify JSON with comments. It will retain comments even after saved!
npm install comment-json


Parse and stringify JSON with comments. It will retain comments even after saved!
- Parse JSON strings with comments into JavaScript objects and MAINTAIN comments
- supports comments everywhere, yes, EVERYWHERE in a JSON file, eventually 😆
- fixes the known issue about comments inside arrays.
- Stringify the objects into JSON strings with comments if there are
The usage of comment-json is exactly the same as the vanilla JSON object.
- Why and How
- Usage and examples
- API reference
- parse
- stringify
- assign
- moveComments
- removeComments
- CommentArray
- Change Logs
There are many other libraries that can deal with JSON with comments, such as json5, or strip-json-comments, but none of them can stringify the parsed object and return back a JSON string the same as the original content.
Imagine that if the user settings are saved in ${library}.json, and the user has written a lot of comments to improve readability. If the library library need to modify the user setting, such as modifying some property values and adding new fields, and if the library uses json5 to read the settings, all comments will disappear after modified which will drive people insane.
So, if you want to parse a JSON string with comments, modify it, then save it back, comment-json is your must choice!
comment-json parse JSON strings with comments and save comment tokens into symbol properties.
For JSON array with comments, comment-json extends the vanilla Array object into CommentArray whose instances could handle comments changes even after a comment array is modified.
``sh`
$ npm i comment-json
~~For TypeScript developers, @types/comment-json could be used~~
Since 2.4.1, comment-json contains typescript declarations, so you might as well remove @types/comment-json.
package.json:
`js`
{
// package name
"name": "comment-json"
}
`js
const {
parse,
stringify,
assign,
moveComments,
removeComments
} = require('comment-json')
const fs = require('fs')
const obj = parse(fs.readFileSync('package.json').toString())
console.log(obj.name) // comment-json
stringify(obj, null, 2)
// Will be the same as package.json, Oh yeah! 😆
// which will be very useful if we use a json file to store configurations.
`
It is a common use case to sort the keys of a JSON file
`js{
const parsed = parse(
// b
"b": 2,
// a
"a": 1
})
// Copy the properties including comments from parsed to the new object {}
// according to the sequence of the given keys
const sorted = assign(
{},
parsed,
// You could also use your custom sorting function
Object.keys(parsed).sort()
)
console.log(stringify(sorted, null, 2))
// {
// // a
// "a": 1,
// // b
// "b": 2
// }
`
For details about assign, see here.
`ts`
parse(text, reviver? = null, remove_comments? = false)
: object | string | number | boolean | null
- text string The string to parse as JSON. See the JSON object for a description of JSON syntax.Function() | null
- reviver? Default to null. It acts the same as the second parameter of JSON.parse. If a function, prescribes how the value originally produced by parsing is transformed, before being returned.comment-json
- also passes the 3rd parameter context to the function reviver, as described in https://github.com/tc39/proposal-json-parse-with-source, which will be useful to parse a JSON string with BigInt values.boolean = false
- remove_comments? If true, the comments won't be maintained, which is often used when we want to get a clean object.
Returns CommentJSONValue (object | string | number | boolean | null) corresponding to the given JSON text.
If the content is:
`js`
/**
before-all
*/
// before-all
{ // before:foo
// before:foo
/ before:foo /
"foo" / after-prop:foo /: // after-colon:foo
1 // after-value:foo
// after-value:foo
, // after:foo
// before:bar
"bar": [ // before:0
// before:0
"baz" // after-value:0
// after-value:0
, // after:0
"quux"
// after:1
] // after:bar
// after:bar
}
// after-all
`js
const {inspect} = require('util')
const parsed = parse(content)
console.log(
inspect(parsed, {
// Since 4.0.0, symbol properties of comments are not enumerable,
// use showHidden: true to print them
showHidden: true
})
)
console.log(Object.keys(parsed))
// > ['foo', 'bar']
console.log(stringify(parsed, null, 2))
// 🚀 Exact as the content above! 🚀
`
And the value of parsed will be:
`js/**
{
// Comments before the JSON object
[Symbol.for('before-all')]: [{
type: 'BlockComment',
value: '\n before-all\n ',
inline: false,
loc: {
// The start location of */
start: {
line: 1,
column: 0
},
// The end location of
end: {
line: 3,
column: 3
}
}
}, {
type: 'LineComment',
value: ' before-all',
inline: false,
loc: ...
}],
...
[Symbol.for('after-prop:foo')]: [{
type: 'BlockComment',
value: ' after-prop:foo ',
inline: true,
loc: ...
}],
// The real value
foo: 1,
bar: [
"baz",
"quux",
// The property of the array
[Symbol.for('after-value:0')]: [{
type: 'LineComment',
value: ' after-value:0',
inline: true,
loc: ...
}, ...],
...
]
}
`
There are NINE kinds of symbol properties:
`js
// Comments before everything
Symbol.for('before-all')
// If all things inside an object or an array are comments
Symbol.for('before')
// comment tokens before
// - a property of an object
// - an item of an array
// and after the previous comma(,) or the opening bracket({ or [)before:${prop}
Symbol.for()
// comment tokens after property key prop and before colon(:)after-prop:${prop}
Symbol.for()
// comment tokens after the colon(:) of property prop and before property valueafter-colon:${prop}
Symbol.for()
// comment tokens after
// - the value of property prop inside an objectprop
// - the item of index inside an array,
// and before the next key-value/item delimiter()}
// or the closing bracket( or ])after-value:${prop}
Symbol.for()
// comment tokens after
// - comma(,)prop
// - the value of property if it is the last propertyafter:${prop}
Symbol.for()
// Always at the inner end of an object or an array,
// only used for stringification
Symbol.for('after')
// Comments after everything
Symbol.for('after-all')
`
And the value of each symbol property is an array of CommentToken
`tsinline
interface CommentToken {
type: 'BlockComment' | 'LineComment'
// The content of the comment, including whitespaces and line breaks
value: string
// If the start location is the same line as the previous token,
// then is true
inline: boolean
// But pay attention that,
// locations will NOT be maintained when stringified
loc: CommentLocation
}
interface CommentLocation {
// The start location begins at the // or /* symbol*/
start: Location
// The end location of multi-line comment ends at the symbol
end: Location
}
interface Location {
line: number
column: number
}
`
comment-json provides a symbol-type called CommentSymbol which can be used for querying comments.CommentDescriptor
Furthermore, a type is provided for enforcing properly formatted symbol names:
`ts
import {
CommentDescriptor, CommentSymbol, parse, CommentArray
} from 'comment-json'
const parsed = parse({ / test / "foo": "bar" })
// typescript only allows properly formatted symbol names here
const symbolName: CommentDescriptor = 'before:foo'
console.log((parsed as CommentArray
`
In this example, casting to Symbol.for(symbolName) to CommentSymbol is mandatory.
Otherwise, TypeScript won't detect that you're trying to query comments.
`js`
console.log(parse(content, null, true))
And the result will be:
`js`
{
foo: 1,
bar: [
"baz",
"quux"
]
}
`js
const parsed = parse(
// comment
1)
console.log(parsed === 1)
// false
`
If we parse a JSON of primative type with remove_comments:false, then the return value of parse() will be of object type.
The value of parsed is equivalent to:
`js
const parsed = new Number(1)
parsed[Symbol.for('before-all')] = [{
type: 'LineComment',
value: ' comment',
inline: false,
loc: ...
}]
`
Which is similar for:
- Boolean typeString
- type
For example
`js
const parsed = parse(
"foo" / comment /)`
Which is equivalent to
`js
const parsed = new String('foo')
parsed[Symbol.for('after-all')] = [{
type: 'BlockComment',
value: ' comment ',
inline: true,
loc: ...
}]
`
But there is one exception:
`js
const parsed = parse(
// comment
null)
console.log(parsed === null) // true
`
`ts`
stringify(object: any, replacer?, space?): string
The arguments are the same as the vanilla JSON.stringify.
And it does the similar thing as the vanilla one, but also deal with extra properties and convert them into comments.
`jscontent
console.log(stringify(parsed, null, 2))
// Exactly the same as `
#### space
If space is not specified, or the space is an empty string, the result of stringify() will have no comments.
For the case above:
`jscode
console.log(stringify(result)) // {"a":1}
console.log(stringify(result, null, 2)) // is the same as `
- target object the target objectobject
- source? the source object. This parameter is optional but it is silly to not pass this argument.Array
- keys? If not specified, all enumerable own properties of source will be used.
This method is used to copy the enumerable own properties and their corresponding comment symbol properties to the target object.
`js// before all
const parsed = parse(
{
// This is a comment
"foo": "bar"
})
const obj = assign({
bar: 'baz'
}, parsed)
stringify(obj, null, 2)
// // before all
// {
// "bar": "baz",
// // This is a comment
// "foo": "bar"
// }
`
But if argument keys is specified and is not empty, then comment before all, which belongs to non-properties, will NOT be copied.
`js
const obj = assign({
bar: 'baz'
}, parsed, ['foo'])
stringify(obj, null, 2)
// {
// "bar": "baz",
// // This is a comment
// "foo": "bar"
// }
`
Specifying the argument keys as an empty array indicates that it will only copy non-property symbols of comments
`js
const obj = assign({
bar: 'baz'
}, parsed, [])
stringify(obj, null, 2)
// // before all
// {
// "bar": "baz",
// }
`
Non-property symbols include:
`js`
Symbol.for('before-all')
Symbol.for('before')
Symbol.for('after') // only for stringify
Symbol.for('after-all')
- source object The source object containing comments to move.object
- target? The target object to move comments to. If not provided, defaults to source (move within same object).object
- from The source comment location.CommentPrefix
- from.where The comment position (e.g., 'before', 'after', 'before-all', etc.).string
- from.key? The property key for property-specific comments. Omit for non-property comments.object
- to The target comment location.CommentPrefix
- to.where The comment position (e.g., 'before', 'after', 'before-all', etc.).string
- to.key? The property key for property-specific comments. Omit for non-property comments.boolean = false
- override? Whether to override existing comments at the target location. If false, comments will be appended.
This method is used to move comments from one location to another within objects. It's particularly useful when you need to reorganize comments or move them between different comment positions.
`js
const {parse, stringify, moveComments} = require('comment-json')
const obj = parse({
"foo": 1, // comment after foo
"bar": 2
})
// Move comment from after 'foo' to after
moveComments(obj, obj,
{ where: 'after', key: 'foo' },
{ where: 'after' }
)
obj.baz = 3
console.log(stringify(obj, null, 2))
// {
// "foo": 1,
// "bar": 2,
// "baz": 3
// // comment after foo
// }
`
`js// top comment
const obj = parse(
{
"foo": 1
})
// Move top comment to bottom
moveComments(obj, obj,
{ where: 'before-all' },
{ where: 'after-all' }
)
console.log(stringify(obj, null, 2))
// {
// "foo": 1
// }
// // top comment
`
`js{
const source = parse(
"foo": 1 // source comment
})
const target = { bar: 2 }
// Move comment from source to target
moveComments(source, target,
{ where: 'after-value', key: 'foo' },
{ where: 'before', key: 'bar' }
)
console.log(stringify(target, null, 2))
// {
// // source comment
// "bar": 2
// }
`
`js{
const obj = parse(
// existing comment
"foo": 1, // another comment
"bar": 2
})
// By default, comments are appended (override = false)
moveComments(obj, obj,
{ where: 'after-value', key: 'foo' },
{ where: 'before', key: 'foo' }
)
console.log(stringify(obj, null, 2))
// {
// // existing comment
// // another comment
// "foo": 1,
// "bar": 2
// }
// With override = true, existing comments are replaced
moveComments(obj, obj,
{ where: 'before', key: 'bar' },
{ where: 'before', key: 'foo' },
true // override existing comments
)
`
- target object The target object to remove comments from.object
- location The comment location to remove.CommentPrefix
- location.where The comment position (e.g., 'before', 'after', 'before-all', etc.).string
- location.key? The property key for property-specific comments. Omit for non-property comments.
This method is used to remove comments from a specific location within objects. It's useful for cleaning up comments or removing unwanted comment annotations.
`js
const {parse, stringify, removeComments} = require('comment-json')
const obj = parse({
// comment before foo
"foo": 1, // comment after foo
"bar": 2
})
// Remove comment before 'foo'
removeComments(obj, { where: 'before', key: 'foo' })
console.log(stringify(obj, null, 2))
// {
// "foo": 1, // comment after foo
// "bar": 2
// }
`
`js// top comment
const obj = parse(
{
"foo": 1
}
// bottom comment)
// Remove top comment
removeComments(obj, { where: 'before-all' })
// Remove bottom comment
removeComments(obj, { where: 'after-all' })
console.log(stringify(obj, null, 2))
// {
// "foo": 1
// }
`
> Advanced Section
All arrays of the parsed object are CommentArrays.
The constructor of CommentArray could be accessed by:
`js`
const {CommentArray} = require('comment-json')
If we modify a comment array, its comment symbol properties could be handled automatically.
`js{
const parsed = parse(
"foo": [
// bar
"bar",
// baz,
"baz"
]
})
parsed.foo.unshift('qux')
stringify(parsed, null, 2)
// {
// "foo": [
// "qux",
// // bar
// "bar",
// // baz
// "baz"
// ]
// }
`
Oh yeah! 😆
But pay attention, if you reassign the property of a comment array with a normal array, all comments will be gone:
`js
parsed.foo = ['quux'].concat(parsed.foo)
stringify(parsed, null, 2)
// {
// "foo": [
// "quux",
// "qux",
// "bar",
// "baz"
// ]
// }
// Whoooops!! 😩 Comments are gone
`
Instead, we should:
`js`
parsed.foo = new CommentArray('quux').concat(parsed.foo)
stringify(parsed, null, 2)
// {
// "foo": [
// "quux",
// "qux",
// // bar
// "bar",
// // baz
// "baz"
// ]
// }
If we have a JSON string str
`js`
{
"foo": "bar", // comment
}
`js`
// When stringify, trailing commas will be eliminated
const stringified = stringify(parse(str), null, 2)
console.log(stringified)
And it will print:
`js`
{
"foo": "bar" // comment
}
s> Advanced Section
comment-json implements the TC39 proposal proposal-json-parse-with-source
`js
const {parse, stringify} = require('comment-json')
const parsed = parse(
{"foo": 9007199254740993},
// The reviver function now has a 3rd param that contains the string source.
(key, value, {source}) =>
/^[0-9]+$/.test(source) ? BigInt(source) : value
)
console.log(parsed)
// {
// "foo": 9007199254740993n
// }
stringify(parsed, (key, val) =>
typeof value === 'bigint'
// Pay attention that
// JSON.rawJSON is supported in node >= 21
? JSON.rawJSON(String(val))
: value
)
// {"foo":9007199254740993}
``
See releases