JSON5 and JSONC: JSON with Comments
Bottom line: Both dialects exist because human-written config needs comments and trailing commas, but their syntax and ecosystem reach differ, so picking wrong jams your toolchain.

Bottom line: use JSONC for comments inside VS Code and TypeScript configuration, use JSON5 when you need the broader ES5.1 literal syntax such as single quotes or unquoted keys, and keep public APIs on standard JSON.
Standard JSON does not allow comments, so every team quietly writes them anyway — until one day the build breaks. Rather than doing it secretly, pick a proper dialect.
1. The two dialects have different motivations
- JSONC (JSON with Comments): standard JSON plus comments and trailing commas, with almost no other syntax changes. It is the de-facto standard of the VS Code ecosystem —
settings.json,tsconfig.json, and.eslintrc.jsonall use it. - JSON5: far more ambitious, aligning directly with the ES5.1 object literal — single quotes, unquoted keys, hexadecimal,
Infinity/NaN, backslash line continuation, and leading+signs.
One sentence to tell them apart: JSONC only makes writing comfortable; JSON5 lets you write a configuration file as if it were JavaScript.
2. Syntax comparison table
| Feature | Standard JSON | JSONC | JSON5 |
|---|---|---|---|
Comments // /* */ |
✗ | ✓ | ✓ |
| Trailing commas | ✗ | ✓ | ✓ |
| Single-quoted strings | ✗ | ✗ | ✓ |
| Unquoted keys | ✗ | ✗ | ✓ |
| Hexadecimal numbers | ✗ | ✗ | ✓ |
Leading/trailing decimal point .5 5. |
✗ | ✗ | ✓ |
Numbers starting with + |
✗ | ✗ | ✓ |
Infinity / NaN |
✗ | ✗ | ✓ |
End-of-line \ continuation |
✗ | ✗ | ✓ |
| Multi-line strings | ✗ | ✗ | ✓ (implemented through continuation) |
| Arbitrary top-level value | ✓ | ✓ | ✓ |
The full JSON5 specification is barely more than a page; the json5 package has long been maintained by the community, and browsers and Node behave identically.
3. Parsing: two APIs, two kinds of positioning ability
3.1 JSON5: simple whole-document parsing
import JSON5 from 'json5';
const config = JSON5.parse(`
{
// service port
port: 8080,
host: '0.0.0.0',
features: ['json5', 'jsonc',],
retries: 0x10,
}
`);
console.log(config.port, config.retries); // 8080 16
JSON5.parse returns a plain object with no offset information — because it is not aimed at editing scenarios, only at "reading configuration".
3.2 JSONC: a syntax tree with offsets
The value of jsonc-parser lies not in parsing but in editing. It offers three layers of API:
| API | Purpose | Typical use |
|---|---|---|
parse |
Parse into a tree carrying offset/length |
Syntax highlighting, folding, navigation |
visit |
Depth-first traversal; the callback receives the node path | JSONPath-like inspection |
modify + applyEdits |
Produce a minimal text edit, then apply it to the original text | Change a value while keeping comments |
import { parseTree, findNodeAtLocation, modify, applyEdits } from 'jsonc-parser';
const text = `{
// port configuration
"server": {
"port": 8080 // 80 in production
}
}`;
const tree = parseTree(text);
const portNode = findNodeAtLocation(tree!, ['server', 'port']);
console.log(portNode?.offset, portNode?.length);
const edits = modify(text, ['server', 'port'], 3000, {
formattingOptions: { insertSpaces: true, tabSize: 2 },
});
console.log(applyEdits(text, edits));
The output keeps the original comments and replaces only the three characters of 8080:
{
// port configuration
"server": {
"port": 3000 // 80 in production
}
}
That is exactly what "editing configuration with regular expressions" cannot do: a regex has no idea which 8080 to change, and no idea that the 8080 inside a string must be left alone.
3.3 Error messages must carry offsets
The error codes from jsonc-parser are enum values; the common ones are:
| Error code | Meaning |
|---|---|
InvalidSymbol |
A character appeared that should not be there |
PropertyNameExpected |
Expected a key name |
ColonExpected |
Expected a colon |
CommaExpected |
Expected a comma |
CloseBraceExpected |
Object was not closed |
UnexpectedEndOfComment |
Comment was not closed |
InvalidEscapeCharacter |
Invalid escape |
Once offset is converted into a line and column, the editor can draw the squiggle exactly on the offending position.
4. When to use which
| Scenario | Recommendation | Reason |
|---|---|---|
| Public API request/response bodies | Standard JSON | Dialects are not supported by general-purpose clients |
package.json, composer.json |
Standard JSON | Package managers only understand standard JSON |
tsconfig.json, .vscode/settings.json |
JSONC | The ecosystem default, natively supported by the toolchain |
| Application configuration (needs comments) | JSONC or YAML | Comments must be preserved while tools can still read and write it |
Needing Infinity, hexadecimal, complex literals |
JSON5 | The strongest expressive power |
| Data exchange, logs, caches | Line-delimited or standard JSON | Streaming and universality are required |
Rule of thumb: if the file will be read by a "non-human tool", prefer standard JSON; only consider a dialect when you are certain it is read solely by your own code.
5. Three kinds of risk that dialects introduce
5.1 Spillover of the requirement
When another tool reads a JSON5 configuration file and calls JSON.parse, it crashes outright. The typical symptom is "works fine locally, fails on Docker" — because the container runs a different reading path.
Countermeasure: add a CI check to the repository that explicitly lists which files are allowed to use a dialect; everything else is validated as standard JSON.
5.2 Surprises around type inference
JSON5 allows unquoted keys and single quotes, but it does not let you bring in literal constructs such as dates or regular expressions — it is still a data format, not JavaScript evaluation.
People have assumed JSON5 could express new Date() only to see the parse fail. JSON5 is not eval.
5.3 Comments silently dropped
Neither JSON5.stringify nor JSON.stringify preserves comments. So "read it in, tweak a value, write it back" loses every comment.
When comments must survive, the only options are the editing API of jsonc-parser, or a YAML library that supports comment preservation.
6. Practical recommendations
- [ ] Keep a "file type → format" table in the team documentation so nobody improvises;
- [ ] Wrap configuration reading in a single
loadConfig()that decides internally which parser to use, so business code never callsJSON.parsedirectly; - [ ] When programmatically modifying a configuration with comments, use
modify+applyEdits; never rewrite the whole file; - [ ] Validate dialect files in CI too (
jsonc-parserfor JSONC,JSON5.parsefor JSON5), otherwise syntax errors only surface at runtime; - [ ] Use standard JSON for public interfaces, cache keys, and message-queue payloads.
FAQ
How should I choose between JSON5 and JSONC?
Choose by runtime rather than by syntax count: VS Code, TypeScript and ESLint read JSONC by default, so JSONC gives the best compatibility; reach for JSON5 only when you need the broader ES5.1 literal syntax, such as single quotes, unquoted keys or Infinity. Neither dialect is appropriate for public API responses.
Why can't a standard JSON parser read JSONC directly?
JSONC allows comments and trailing commas, while JSON.parse accepts only RFC 8259 JSON and throws on either. Reading JSONC requires a parser such as jsonc-parser, or removing comments before handing the text to a standard parser.
Should comments in JSONC survive edits to the file?
Yes, but only through an editing API. jsonc-parser's modify and applyEdits change the targeted node while preserving the remaining comments; parsing to an object and stringifying it drops every comment.