Finding the Exact Line of a JSON Syntax Error in Any Parser
Every JSON parser reports a position in a different way. How to turn a character offset into a line, a column and a caret in Python, Node and jq.

A parser says Expecting ',' delimiter: line 5 column 3 (char 53) and you open a file of three thousand lines. The message looks precise, but the number only helps once you know what it counts, which character it points at and why the true mistake is so often somewhere else. This article is about reading those coordinates. It covers what Python, Node and jq each report, how to turn an offset into a line and column, a small helper that prints the line with a caret, and the cases where the line is a good place to start looking and a bad place to end.
The file I used
The test file is short on purpose, so the coordinates are easy to check by eye. Line 4 ends without a comma, and line 5 starts the next key:
{
"id": 7,
"name": "Ada",
"tags": ["a", "b"]
"active": true
}Everything before line 4 is valid, and line 4 is valid up to its closing bracket. The parser only discovers a problem when it reads the " that begins line 5 and finds it where a comma or a closing brace should be. That is the first fact worth keeping: a syntax error is reported where the parser gave up, not where you made the mistake. For a missing comma those two places are on adjacent lines.
What each tool reports
Here is the same file through three tools:
Python 3.13 Expecting ',' delimiter: line 5 column 3 (char 53)
Node 24 Expected ',' or '}' after property value in JSON at position 53 (line 5 column 3)
jq 1.6 parse error: Expected separator between values at line 5, column 10Python and Node agree. Line 5, column 3 is the opening quote of "active", two spaces in, and character 53 is the offset counted from zero. The offset is how the parser sees the text, a single running count of characters, and the line and column are a convenience derived from it.
jq says column 10, which looks like a disagreement and is not a bug. jq reports where it was when it noticed, which is after it read the token "active" and met the colon, so it is a few characters further along. When you work between tools, trust the line number to within one and treat the column as a rough pointer. If you check with a second parser and the columns differ, that tells you nothing is wrong with the file beyond what the first said.
Node deserves a note on versions. Recent releases (this run was 24.22.1) add (line 5 column 3) to the message. Older ones print only at position 53, and some older ones print Unexpected token " in JSON at position 53 with the offending character in the message. All of them count characters from zero, and all of them count a newline as one character, which matters in the next section.
Turning an offset into a line and column
If a tool gives you only an offset, count the newlines before it. The line is one plus the number of newlines in the text up to the offset, and the column is the distance from the last newline. In JavaScript:
function lineCol(text, pos) {
const before = text.slice(0, pos).split("\n");
return { line: before.length, col: before[before.length - 1].length + 1 };
}On the test file and position 53, that returned { line: 5, col: 3 }, matching the line and column that Python computed.
Two traps are worth knowing. The first is line endings. A file saved with Windows line endings has two characters, CR and LF, at the end of each line, and an editor that counts "characters" may count them differently than the parser. If your offset lands a few characters off in an editor's "go to character" feature, that is the usual reason, and the line number from the parser is the safer coordinate. The second is non-ASCII text. Python counts code points, so é is one position. JavaScript counts UTF-16 code units, so an emoji outside the Basic Multilingual Plane counts as two. On a file with many emoji before the error, a Node offset and a Python offset for the same fault can differ by the number of those characters.
A helper that shows the line
Rather than calculating by hand, a short function prints the line with a caret. Python's JSONDecodeError has lineno, colno, pos and msg as attributes, so no parsing of the message is needed:
import json
def where(text, e):
lines = text.split("\n")
print(f"{type(e).__name__}: {e.msg} (line {e.lineno}, column {e.colno}, char {e.pos})")
print(f"{e.lineno:>4} | {lines[e.lineno - 1]}")
print(" | " + " " * (e.colno - 1) + "^")
text = open("bad.json").read()
try:
json.loads(text)
except json.JSONDecodeError as e:
where(text, e)The output on the test file:
JSONDecodeError: Expecting ',' delimiter (line 5, column 3, char 53)
5 | "active": true
| ^Add one more print for line e.lineno - 1, and the missing comma at the end of line 4 is in view at the same time. In practice I print three lines, the one before, the failing one and the one after, because the cause is one of the first two nearly every time.
For quick checks with no script, python3 -m json.tool bad.json prints the same message without any setup, and jq . bad.json does it with jq's coordinates. Both are in nearly every environment where you will be debugging.
Reading the position correctly
Once you have a line and column, the question is what mistake could have put the parser there. Some patterns repeat enough to memorize:
**
Expecting ',' delimiter, at the start of a token.** A comma is missing after the previous value. Look at the end of the line above.**
Expecting property name enclosed in double quotes, right after a comma.** A trailing comma before}, or a key in single quotes or with no quotes. The position points at the offending character itself. In Python 3.13 a trailing comma in an array has its own wording,Illegal trailing comma before end of array, reported at the comma's position (for[1, 2, 3,]it said column 23 on my test string, the position of the comma).**
Expecting valueat the start of a line or after a colon.** Something that is not a value is where a value should be:undefined, a bare word, a comment, an empty string between two commas.**
Extra data.** The document ended and something followed it. The position is the first byte after the end of the first valid value, and it usually means two documents were glued together, covered in Python JSONDecodeError extra data and concatenated objects.**
Invalid control character.** A raw newline or tab inside a string. The position is that character, not the start of the string. See fixing JSON with unescaped newlines in strings.
The full list of messages and what each one means is in fixing JSON parse errors by reading the message properly.
Position zero and the end of the text
Two positions deserve a separate look because the line is not the useful part. A position of 0, or line 1 column 1, means the first character is not JSON at all. The text is empty, or it is an HTML error page, or it starts with a byte order mark or the word undefined. The question to ask is where the text came from, which is the subject of unexpected token in JSON at position 0. A position at the very end of the text (equal to its length) means the document stopped early, so a closing bracket is missing or the text was cut off, covered in unexpected end of JSON input.
In both cases scanning the middle of the file wastes time, because the file is not wrong in the middle.
Large files
When the file is large, opening it in an editor and jumping to the line works up to a point. A multi-megabyte JSON file is often a single line, because it was minified, and then "line 1, column 2,884,113" is the only coordinate there is. Two approaches help. Print a window around the offset instead of a line: text[pos - 80 : pos + 80] shows the few tokens on either side of the failure, which is usually enough to see the comma. Or reformat what does parse, which is not possible until the fault is fixed, so instead use a tool that tolerates the fault long enough to show you: jq with --stream emits events up to the point of the error and then stops, so the last events before the message show the last good path. On the test file, that path was the tags array, which is exactly where the comma went missing. The JSON formatter on this site underlines the failing position in the same way for pasted documents, and everything runs in your browser.
When the reported line is not the broken line
A line number is a coordinate on the page, and it will mislead you in a few situations.
The mistake is earlier than the error. An unclosed string swallows text until the next quote. If line 40 forgets the closing quote, the parser treats the rest of line 40 and the beginning of line 41 as string content, and the error shows up on line 41 as an unexpected token. The broken line is 40. The same happens with a missing closing brace: the document is valid until the parser runs out of text, and the error lands at the end of the file, far from the object you forgot to close. An editor that matches brackets and highlights the unmatched one finds that faster than reading.
The file is not the text that was parsed. If the application reads a file, edits it in memory, perhaps by templating or string concatenation, and parses the result, the line number refers to the result. Log the text that was parsed, not the file name.
The line is in the middle of an escaped string. If JSON was embedded in JSON (a string field that holds a document), the inner parser's line number means nothing in the outer file. Parse the inner string on its own, print it, and read the coordinates there.
Another tool made the change. A pretty-printer that fails silently or a proxy that rewrites a body can change the line structure between the file you edited and the text the parser saw. If the coordinates cannot be reconciled with the file, compare what you sent with what was received.
When a position makes no sense, move away from the position and ask what changed the text between its author and its reader. That was the faster route in every one of these cases I tested, and it is the same advice that applies to JSON parse errors with a smart quote or invisible character, where the character at the reported position looks fine on screen.
References
The parser behaviors in this article are documented in the Python json module reference for JSONDecodeError and its attributes, and in the MDN page on JSON.parse for the exceptions it throws. The grammar that decides what counts as a mistake is in RFC 8259.


