The JSON.parse error points one step past the mistake: common JSON errors

Trailing commas, single quotes, comments and NaN that JSON parsers reject, big integers and duplicate keys that change silently, and how to read an error position, based on RFC 8259.

DevelopmentData

You tweak a config file, restart the service, and get this:

SyntaxError: Expected double-quoted property name in JSON at position 46 (line 4 column 1)

Line 4 holds a single closing brace. There is nothing to fix there. The real mistake is one line up: a comma left after the last item.

The position is where the parser gave up

A JSON parser reads left to right and stops at the first character the grammar doesn’t allow. The comma was fine on its own; after a comma the grammar expects another member, and it found } instead. So it reports the }.

Diagram of a JSON error position. In four lines of JSON, line 1 is an opening brace, line 2 is “name”: “Motitory”, with a comma, line 3 is “tags”: [“a”, “b”] followed by a comma, and line 4 is the closing brace. The cause is the comma at the end of line 3, but the parser stops at the closing brace on line 4, column 1, and reports position 46. The reported spot is where the parser gave up; the cause is usually just before it.

So when you get a position, look at it and just before it. position is a zero-based character offset (strictly, UTF-16 code units, so one emoji counts as 2). Recent Node and Chrome add (line 4 column 1); other versions give only the offset, as in Unexpected token } in JSON at position 46, or quote the surrounding text instead. The wording varies by engine, so the position is the part to rely on.

Fine in JavaScript, invalid in JSON

Most errors come from writing JSON as though it were a JavaScript object literal. They look alike, but JSON’s grammar (RFC 8259, ECMA-404) is much narrower.

You wrote JavaScript object JSON
Trailing comma [1, 2,] Allowed Error
Single quotes {'a': 1} Allowed Error: strings and keys take double quotes only
Unquoted key {a: 1} Allowed Error
Comments // …, /* … */ Allowed Error: the grammar has no comments
NaN, Infinity, undefined Usable values Error: not JSON values
012, 0x1F, .5, +1 Some allowed Error: no leading zeros, hex, bare decimal point or + sign
A literal tab or newline inside a string Allowed in template literals Error: write \t, \n

The last row bites when you paste a cell from a spreadsheet into a JSON string. It looks like a space but it is a tab, and V8 says Bad control character in string literal.

A BOM (EF BB BF) at the very start of the file is another classic. Windows Notepad and some editors add one to UTF-8 files, and you can’t see it. RFC 8259 says not to add one, and allows parsers to ignore it. In practice they differ: JSON.parse fails on the first character, while Python’s json.loads helpfully says to decode with utf-8-sig.

Accepted, but changed

Worse are the cases where the parser says nothing and the data still comes out different.

Big integers. The JSON grammar puts no limit on a number’s size. But JSON.parse reads numbers as a JavaScript Number, a double, so integers above 2⁵³ are rounded to the nearest one it can hold.

JSON.parse('{"id": 9007199254740993}').id; // 9007199254740992

An order number or user ID arrives one digit off, and nothing complains. This is why many APIs send 64-bit IDs as strings; the floating-point article explains the mechanism. Read the same JSON in Python and the integer survives intact, because Python’s int has no size limit. The same bytes, two different values.

Duplicate keys. {"a": 1, "a": 2} is not a syntax error. RFC 8259 only says names SHOULD be unique and warns that behavior with duplicates is unpredictable. JavaScript and Python both keep the last value, 2; other parsers keep the first or reject the document. Merge two config files, end up with a key twice, and you can spend a long time wondering why your edit near the top is ignored.

Parsers that accept NaN. Python’s json module reads and writes NaN and Infinity by default. json.dumps(float("nan")) produces NaN, which isn’t standard JSON, and JSON.parse on the JavaScript side then fails. If Python is the sender, allow_nan=False stops it at the source.

What JSON.stringify drops. It goes the other way too. Properties holding undefined or a function disappear (inside arrays they become null), and NaN and Infinity become null wherever they are. A Date becomes an ISO string, and a BigInt throws (Do not know how to serialize a BigInt). If the object you sent and the one that arrived have different keys, check these rules first.

If you want comments

You can put comments in tsconfig.json and VS Code’s settings.json. Those files only borrow the .json extension: they are JSONC (JSON with Comments), which also tolerates trailing commas, and each tool reads them with its own parser. Feed one to JSON.parse and it fails. For hand-edited config that needs comments, use a JSONC or JSON5 parser; keep the data that programs exchange as plain JSON.

Let a tool find the spot

Paste JSON into the JSON formatter and validator and it reindents it, or, if the syntax is wrong, points to the exact line and column, even when the browser’s own error doesn’t say. Numbers are kept exactly as written, so big integers aren’t rounded, and duplicate keys get their own warning. Paste the four lines from the diagram and it reports line 4, column 1; delete the comma at the end of line 3 and it passes.

References

  • RFC 8259, The JavaScript Object Notation (JSON) Data Interchange Format
  • ECMA-404, The JSON Data Interchange Syntax
  • ECMAScript specification (ECMA-262), JSON.parse and JSON.stringify
  • Python documentation, json — JSON encoder and decoder

Back to the blog