# Why your JSON won't parse, and how to find the character

> Almost every JSON parse failure is one of five things: a trailing comma, single quotes, unquoted keys, a literal newline inside a string, or comments. JSON allows none of them. The reported error position is usually one token after the actual mistake.

Source: https://rankcert.com/blog/json-formatting-and-validation
Published: 2026-08-29 · Updated: 2026-09-02

---


JSON has one of the smallest grammars of any data format, which is exactly why it fails in ways that look absurd. Almost every parse error is one of five things.

## The five reasons JSON fails

### 1. A trailing comma

```json
{ "name": "RankCert", "verified": true, }
```

Legal in JavaScript object literals, legal in JSON5, legal in most config formats. Not legal in JSON. This is by far the most common cause, because every developer's fingers are trained by JavaScript.

### 2. Single quotes

```json
{ 'name': 'RankCert' }
```

JSON strings are double-quoted. Always. Both keys and values. There is no exception.

### 3. Unquoted keys

```json
{ name: "RankCert" }
```

Valid JavaScript, invalid JSON. Keys are strings, so they take quotes.

### 4. A literal newline inside a string

```json
{ "note": "line one
line two" }
```

Control characters below U+0020 must be escaped. Write `\n`, not an actual line break.

### 5. Comments

```json
{
  // the product name
  "name": "RankCert"
}
```

JSON has no comment syntax. Neither `//` nor `/* */`. If you need commented config, you need JSONC, YAML or TOML - not JSON.

Free tool: [JSON Formatter](https://rankcert.com/tools/json-formatter) - Format, validate and minify JSON in your browser. Get the exact line and column of a syntax error instead of a vague parse failure.

## Reading the error message

`JSON.parse` reports a character offset, not a line number, which is useless on a 4,000-character payload. Convert it:

```js
try {
  JSON.parse(source);
} catch (error) {
  const position = Number(String(error.message).match(/position (\d+)/)?.[1]);
  const before = source.slice(0, position);
  const line = before.split("\n").length;
  const column = position - before.lastIndexOf("\n");
  console.error(`${error.message} - line ${line}, column ${column}`);
}
```

Note that the reported position is usually where the parser *gave up*, which is one token after the actual mistake. A trailing comma on line 12 typically reports at the closing brace on line 13. Look at the line before the one you are given.

## Things JSON allows that surprise people

**Duplicate keys are legal.** The spec does not forbid them; it leaves the behaviour to the implementation. `JSON.parse` keeps the last one silently. If your API accepts JSON from clients, a duplicate key is a real attack surface - two parsers in your stack may disagree about which value won.

**Top-level scalars are valid.** `42`, `"hello"` and `null` are each a complete, valid JSON document. Not every parser written before 2014 agrees.

**Key order is preserved in practice.** The spec says objects are unordered, but every mainstream parser preserves insertion order. Do not rely on it across languages.

**Numbers have no defined precision.** `JSON.parse` produces IEEE 754 doubles, so any integer above 2^53 loses precision silently. A 64-bit database ID sent as a JSON number will come back wrong. Send large IDs as strings.

## Things JSON does not allow that you might expect

- `NaN`, `Infinity`, `-Infinity` - not valid. `JSON.stringify` converts them to `null` without warning.
- `undefined` - dropped entirely from objects, converted to `null` inside arrays.
- Dates - there is no date type. `JSON.stringify(new Date())` produces an ISO string, and parsing gives you a string back, not a Date.
- `BigInt` - throws on stringify. Convert to a string first.
- Trailing decimal points - `1.` is invalid; write `1.0`.
- Leading zeros - `007` is invalid.

## Minifying, and when it matters

Whitespace typically accounts for 15–30% of a formatted JSON payload. For anything you transmit, minify it and let gzip do the rest - though note that gzip already handles repeated whitespace well, so the real-world saving after compression is smaller than the byte count suggests.

Where minifying genuinely matters is anywhere the payload is stored uncompressed at scale: a `jsonb` column, a log line, a cache entry.

For anything a human reads - config files, fixtures, API examples - format with two spaces and never think about it again.

## Validating structure, not just syntax

A document can be perfectly valid JSON and still be wrong for your use. Syntax validation says the braces match; it says nothing about whether `email` is present or `age` is a number.

For that you want a schema. JSON Schema is the portable option; in TypeScript, a runtime validator like Zod gives you both the check and the inferred type from one declaration. Validate at every boundary where data enters your system, because a parser will happily hand you `{}` when you expected a user.

<Cta />
