Skip to content
jsonbeautifiers
English

JSON has no comments, and that was on purpose

Comments were cut from JSON to protect interoperability, and that decision has cost every configuration file since.

Every claim here is either measured or sourced. Where it is neither, it says so.

You add a line to a config file explaining why a timeout is 45 seconds and not 30, the deploy fails, and the message is not helpful:

JSON.parse('{\n  // 45s: upstream p99 is 38s\n  "timeout": 45\n}')
// Expected property name or '}' in JSON at position 4 (line 2 column 3)

The parser saw a slash where a key should be and gave up. Python is no clearer about the cause:

json.loads('{\n  // 45s: upstream p99 is 38s\n  "timeout": 45\n}')
# JSONDecodeError: Expecting property name enclosed in double quotes: line 2 column 3 (char 4)

Neither message mentions comments, because as far as the grammar is concerned there is nothing to mention. RFC 8259 defines exactly four characters that may appear between tokens: space, tab, carriage return and line feed. Everything else is either part of a value or a syntax error.

Why they were removed

Comments were in early JSON and Douglas Crockford took them out. His stated reason is the interesting part: people were not using them for prose, they were using them to carry parsing directives. Something like an encoding hint or a schema pointer, written in a comment, which one particular consumer would read and act on. At that point the comment is not a comment. It is a second, undocumented data channel riding inside a format whose whole selling point was that any parser anywhere would read the same values out of the same bytes.

Crockford’s own suggested workaround was to pipe your commented file through a minifier before handing it to a parser. That is still the right shape of answer, and the rest of this article is mostly about doing it correctly.

The decision was defensible for what JSON was in its early years: a wire format for moving a value between two programs that had already agreed on its meaning. Nobody comments a network packet.

Why it hurts anyway

JSON did not stay a wire format. It became the default configuration language for the entire toolchain, and configuration is exactly the case where the reasoning behind a value matters more than the value. A retries: 0 with no explanation gets helpfully “fixed” by the next person. A retries: 0 with // intentional, this endpoint is not idempotent above it does not.

So every ecosystem that adopted JSON for config has grown its own patch on top, and they are not compatible with each other.

The five options

A _comment key

{
  "_comment": "45s because upstream p99 is 38s",
  "timeout": 45
}

It is strict JSON, it parses everywhere, and it needs no tooling. The problems are real though. Your schema now has to allow it or your validator rejects it. It is data, so it ships to clients, gets logged, and shows up in diffs as a value change rather than a comment change. And you get exactly one per object: RFC 8259 says keys SHOULD be unique and leaves duplicates undefined, with JavaScript and Python both keeping the last one, so a second _comment at the same level silently eats the first. People work around that with _comment1, _comment2, which is the point at which the approach stops paying for itself.

Use it for a single header note at the top of a file. Do not use it for line-by-line annotation.

JSONC

JSONC is JSON plus two things: // and /* */ comments, and trailing commas. Nothing else. It is what VS Code uses for its own settings.json and keybindings.json, and what TypeScript accepts in tsconfig.json.

{
  // upstream p99 is 38s
  "timeout": 45,
  "retries": 0, // this endpoint is not idempotent
}

Worth being blunt about the status: there is no standalone specification for JSONC. There is no RFC, no version number and no conformance suite. It is a convention with an editor-shaped implementation behind it, and dialects vary at the edges (whether a trailing comma is allowed after the last array element, whether comments are preserved on a round trip). It is the safest choice when your consumer is already a tool that supports it, and a bad choice for anything you hand to a third party.

JSON5

JSON5 is a real specification with a version history, and it extends considerably further than JSONC:

  • Unquoted object keys, where the key is a valid ES5 identifier
  • Single-quoted strings
  • Trailing commas in objects and arrays
  • Line and block comments
  • Hexadecimal numbers
  • Leading and trailing decimal points, so .5 and 5. are numbers
  • Infinity, -Infinity and NaN

The last item is the one to think hard about. NaN and infinities have no representation in JSON at all, so a JSON5 document using them cannot be converted to JSON without a lossy decision about what to put in their place. The rest of the extensions are cosmetic and survive a conversion fine. Use JSON5 when a human is the primary author of the file and a .json5 extension is acceptable; do not use it as an API format.

Stop using JSON

If the file is configuration you own from end to end, and nothing external consumes it, the format is a free choice and JSON is not obviously the best one. YAML and TOML both have first-class comments. Both have their own costs, and the comparison is worth reading before you commit, because YAML in particular will hand you the Norway problem: under YAML 1.1 semantics, which PyYAML and Ruby’s Psych implement, a bare no parses as the boolean false.

Strip at build time

Keep the annotated file as the source of truth, strip the comments in CI, ship strict JSON. This is Crockford’s suggestion and it composes with everything: your editors and reviewers see the comments, your runtime parser sees a document that satisfies RFC 8259, and no consumer needs to know either format exists.

Stripping comments without breaking URLs

The obvious implementation is a regex, and the obvious regex is wrong:

// Do not do this.
text.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');

Run it on this and watch it destroy a value:

{
  "endpoint": "https://api.example.com/v2/orders", // production
  "note": "see /* the runbook */ before changing"
}

The first rule finds // inside https:// and deletes the rest of the line including the closing quote and comma. The second finds a block comment inside a string. You end up with an unterminated string and an error that points nowhere near the mistake. A regex cannot do this job because it cannot tell whether a slash is inside a string, and string context in JSON depends on counting escapes.

You need a scanner that tracks exactly one piece of state:

function stripJsonComments(text) {
  let out = '';
  let inString = false;
  let inLine = false;
  let inBlock = false;

  for (let i = 0; i < text.length; i++) {
    const c = text[i];
    const next = text[i + 1];

    if (inLine) {
      if (c === '\n') { inLine = false; out += c; }
      continue;
    }
    if (inBlock) {
      // keep newlines so error line numbers still match the source
      if (c === '*' && next === '/') { inBlock = false; i++; }
      else if (c === '\n') { out += c; }
      continue;
    }
    if (inString) {
      out += c;
      if (c === '\\') { out += next; i++; continue; }  // escape, consume both
      if (c === '"') inString = false;
      continue;
    }
    if (c === '"') { inString = true; out += c; continue; }
    if (c === '/' && next === '/') { inLine = true; i++; continue; }
    if (c === '/' && next === '*') { inBlock = true; i++; continue; }
    out += c;
  }
  return out;
}

The escape branch is the part people leave out. Without it, the escaped quote in "he said \"go to https://example.com\" today" reads as the end of the string, so the // that follows is treated as the start of a comment and the rest of the line is deleted.

Note also what this does not do. Stripping comments leaves trailing commas behind, and those fail separately with their own error: V8 reports Expected double-quoted property name in JSON at position 7 (line 1 column 8) for {"a":1,}, and the entirely different Unexpected token ']', "[1,2,]" is not valid JSON for [1,2,]. A JSONC to JSON conversion has to handle both.

If you would rather not carry the scanner, paste the file into JSON Repair, which removes comments and trailing commas in one pass and hands back strict JSON, then confirm the result against the validator. Both run entirely in your browser, which matters when the file you are fixing is a production config with credentials in it. The other error pages cover what to do when the failure turns out not to be a comment at all.