Skip to content
← escentmachine field notes

Reading JSON Syntax Error Messages: Line, Column, and Position

Contents
AI-generated illustration; not taken on location · Illustration of a code editor window with a red error marker on a line of JSON text.
AI-generated illustration; not taken on location · Illustration of a code editor highlighting a JSON syntax error for easier location.AI-generated illustration

JSON errors look like diagnoses, but they are really parser state reports. A parser reads tokens from left to right and stops at the first point where its grammar cannot continue. That point is often the token after the real mistake: a missing comma is noticed at the next key, a trailing comma is noticed at the closing brace, and a missing brace is noticed only at end of input.

This guide explains how to read the message, line, column, and position fields; why the same character can be described as column 2 by one field and position 1 by another; why byte offsets, character offsets, and code-unit offsets are not interchangeable; and how to map a reported location back to an editor. It uses malformed examples you can paste into escentmachine’s JSON Inspector, then recommends an error panel that shows location, reason, and next step without silently repairing the input.

The working rule is simple: Inspect data. Find the mismatch. Start with the earliest reported location, then look at the character just before it.

Error anatomy: message, line, column, position

Every JSON syntax error message contains at least one of four things: a human-readable reason, a line number, a column number, and a position or offset. They answer different questions.

Field What it usually means What to check
Message The grammar rule the parser was trying to satisfy when it stopped Treat it as a parser state, not a complete diagnosis
Line The line in the decoded text Start at the earliest line, not the last error in a cascade
Column The character within that line Inspect the character just before the reported column
Position / offset A numeric index Identify whether it counts bytes, characters, or code units, and where it starts counting

The key distinction is between the reported point and the actual mistake. A parser tokenizes sequentially. If a comma is missing between two members, the parser may not fail until it reaches the next member name, because a quoted key is a valid way to start a member but not a valid way to continue a value. If a trailing comma appears before }, the parser may fail at } because it expected another member name after the comma. In both cases, the useful character is behind the caret.

This is why a JSON error panel should not say only "invalid JSON at line 1, column 12." It should show the exact source line, place a caret at the reported column, and prompt the reader to inspect the character before it. That one habit resolves many trailing-comma, missing-comma, and quote errors faster than reading the parser message alone.

Malformed JSON catalog with expected parser output

Below are common malformed documents and the kind of parser output they produce. Exact wording varies by parser and version, but the shape of the failure is stable. Use these as debugging patterns, not as a message catalog.

Trailing comma

{"a": 1,}

A parser that stops here has usually reached the } while still expecting another member name. The comma after a member is not the problem by itself; what is missing is the member that the comma promises. Remove the comma before }. The same applies to [1, 2, 3,]; an MDN example of a trailing comma in an array reports a SyntaxError at line 1 column 14.

Unquoted key

{foo: 1}

JSON requires object member names to be double-quoted strings. A JavaScript object literal accepts foo, but JSON does not. MDN’s JSON.parse example for {'foo': 1} reports a SyntaxError expecting a property name at line 1 column 2. Change it to {"foo": 1}.

Single-quoted key or string

{'foo': 1}

Single quotes are not JSON string delimiters. A typical report is a property-name error or an expected double-quoted property name at the single quote. Replace single quotes with double quotes, and escape any double quote inside the string.

Missing brace

{"a": 1

There is no closing }. Whatever message the parser produces, do not simply append a brace at the end. Check indentation and count opening and closing braces at each nesting level, because an unbalanced inner object will still fail after you close the outer one.

Invalid escape

{"a": "\x"}

JSON strings must escape quotation marks and backslashes. Literal control characters and invented escapes such as \x are invalid, and a line break inside a string must be written as \n. The parser may report an invalid escape or a bad control character near the backslash.

Leading zero

{"a": 01}

JSON numbers do not allow leading zeros, and JSON.parse rejects them. MDN reports line 1 column 2 for its {"foo": 01} example. Write 1, or quote the value if it is an identifier such as a ZIP code.

Incomplete decimal

{"a": 1.}

A decimal point must be followed by at least one digit, and JSON.parse rejects a bare trailing point. MDN reports line 1 column 2 for its {"foo": 1.} example. Write 1.0 or 1.

Missing comma

{"a": 1 "b": 2}

The parser may report the problem at "b", but the missing comma is before it. Add a comma after 1. This is the most important catalog entry because it shows why the reported location is not always the mistake location.

1-based versus 0-based indexing

Line, column, and position are different coordinate systems, and tools do not all agree about where to start counting. A line and column pair is a presentation coordinate; a position is a programmatic index. Some tools number lines and columns from 1 and offsets from 0, and some do something else entirely. The numbers can therefore differ by one for the same character without anyone disagreeing about where the error is.

The classic illustration is a report of line 1, column 2 (char 1). The second character of the first line is described as column 2 by a counter that starts at 1, and as index 1 by a counter that starts at 0. Both descriptions identify the same character.

Python is a useful case because the two surfaces do not behave identically. json.loads raises JSONDecodeError when the deserialized data is not a valid JSON document, and json.tool prints a character index. Because the indexing convention is a property of the tool rather than of JSON, confirm it in the documentation for the version you are running instead of assuming a rule from a message you saw elsewhere.

A safe conversion strategy is to normalize to a character offset first, then derive everything else:

offset = 1  # zero-based index of the second character
line = 1
column = offset + 1
print(line, column, offset)  # 1 2 1

The arithmetic above is an illustration of the convention gap, not a claim about any specific parser’s output. If a tool reports line 1, column 2, it is using a 1-based column. If it reports position 1 for the same character, it is using a 0-based position. Both can be correct. The UI should label the convention rather than silently adding or subtracting one.

Byte offsets versus character offsets

An offset means nothing without a unit. JSON text is Unicode, so the same error can be numbered differently depending on whether the tool counts bytes, Unicode code points, or UTF-16 code units.

  • A byte offset counts the encoded bytes of the document. In UTF-8, a non-ASCII character occupies more than one byte, so byte counts run ahead of visible character counts once the text leaves ASCII.
  • A character offset usually counts Unicode code points, which matches what a reader perceives as characters.
  • A code-unit offset counts the units a string type stores internally. JavaScript strings use UTF-16 code units, so a character outside the Basic Multilingual Plane advances the count by two.

The same document can therefore produce different numeric offsets in two tools without either tool being wrong. nlohmann/json stores a byte index in its exception, while Python’s json.tool prints a character index. That single difference is enough to make a copy-pasted offset land in the wrong place when you switch tools.

Escaping adds another layer. A literal character and its \uXXXX escape denote the same key but occupy different numbers of bytes and characters in the source text. Two documents that mean the same thing are not the same length, so only compare offsets computed over the same representation.

If a byte offset lands in the middle of a multibyte character, treat that as a signal that the offset basis or the input bytes need inspection. Do not silently round it to a character boundary and pretend the position is exact.

Mapping parser positions back to the editor

Mapping a parser position to an editor is a small normalization problem. The goal is to place a visible cursor at the parser’s reported point, then let the reader inspect the preceding character.

  1. Identify the basis: does the tool count lines from 1 or 0, columns from 1 or 0, and offsets from 0 or 1? Which unit does the offset use?
  2. Convert a byte offset to a character offset by decoding the prefix as UTF-8. If the prefix is invalid, show the byte view instead of guessing.
  3. Convert a code-unit offset to code points only if the editor displays code points. If the editor uses UTF-16 internally, the offset may already be the right unit.
  4. Build a line-start table from the decoded text, handling \n, \r\n, and \r consistently.
  5. Convert the character offset to line and column: line is the count of line breaks before the offset plus one; column is the distance from the last line start plus one.
  6. Convert the logical column to a visual column: expand tabs to the editor’s tab stops, and account for wide characters and combining marks.
  7. Place the caret at the reported point, but highlight the character before it as the first place to inspect.

A minimal JavaScript helper for a UTF-16 string looks like this:

function lineColFromOffset(text, offset) {
  const before = text.slice(0, offset);
  const lines = before.split('\n');
  return {
    line: lines.length,
    column: lines[lines.length - 1].length + 1
  };
}

This helper handles LF only; production code should normalize CRLF and CR first. It also counts UTF-16 code units because JavaScript strings do. If the parser reported a byte offset, decode the bytes to a string before using it. If the reported offset is at the end of the text, the caret belongs after the last character, and the first thing to inspect is the last token, not a missing character at the cursor.

Parser-specific examples: JavaScript, Python, C++

Exact error wording is not standardized. The JSON grammar is fixed, but each parser chooses its own message, location convention, and unit. The table below lists only what each library documents; it is not a promise about the message you will see.

Parser Input Documented behavior Location basis
JavaScript JSON.parse {'foo': 1} SyntaxError expecting a property name; MDN reports line 1 column 2 Line and column as reported in the message
JavaScript JSON.parse [1, 2, 3,] Trailing comma rejected; MDN reports line 1 column 14 for its array example Line and column as reported in the message
JavaScript JSON.parse {"foo": 01} Leading zero rejected; MDN reports line 1 column 2 Line and column as reported in the message
JavaScript JSON.parse {"foo": 1.} Incomplete decimal rejected; MDN reports line 1 column 2 Line and column as reported in the message
Python json.loads any invalid document Raises JSONDecodeError Message carries location information
Python json.tool any invalid document Prints a character index Character offset
nlohmann/json any invalid document parse_error whose message contains line and column Byte index stored in the exception, from which both are computed

The practical lesson is not to memorize messages. The lesson is to ask three questions: what unit is the offset in, where does each counter start, and is the reported point at the mistake or after it? Once those are answered, a JavaScript message, a Python traceback, and a C++ exception can all be mapped to the same caret position. Do not treat any row as a stable API contract; run the parser version you actually use and record its output.

False alarms: non-JSON content and line 1 column 1

Some "JSON errors" are not JSON errors at all. When a parser reports position 0, char 0, or line 1 column 1, the first hypothesis should be that the parser did not receive the JSON document you think it did.

Common causes:

  • An HTML error page was returned instead of JSON. The first character is <, so the parser fails at line 1 column 1.
  • The response body is empty. There is nothing to tokenize, and the parser fails immediately.
  • A filename was passed to the parser instead of the file contents. The parser sees the first letter of the path and fails at line 1 column 1.
  • The file was never opened, so the parser received zero characters. A C++ parser can report parse_error.101 at line 1, column 1 for a file that is perfectly valid on disk.
  • The HTTP status is an error, but the client tries to parse the error page as JSON. Check status and content type before parsing.

A good UI distinguishes "not JSON" from "invalid JSON." For a non-JSON response, show the status, content type, and a short excerpt of the first bytes. For an empty body, say so explicitly. For a file, show the path and size. Do not extract something that looks like JSON from an HTML page, and do not wrap the input in quotes to make it parse. Those are silent repairs that hide the real mismatch.

UI and editorial recommendations for escentmachine

escentmachine’s JSON Inspector should treat a syntax error as an inspection result, not a failure to be auto-corrected. The error panel should have three explicit rows.

Row Content Example
Location Line, column, and offset with units and indexing stated Line 1, column 2; character offset 1
Reason Raw parser message plus a plain-language summary Expected property name; single-quoted keys are not valid JSON
Next step One concrete action, with the preceding character highlighted Inspect the quote after {; replace single quotes with double quotes

The panel should show the exact source line in a monospace editor, place a caret at the reported location, and highlight the character before it when the parser is likely reporting after the mistake. It should preserve exact tokens: do not rewrite single quotes, remove trailing commas, or insert braces unless the user explicitly asks for a suggestion. If the tool offers fixes, label them as suggestions and require a click.

Use the brand’s visual language: a dark editor, cyan accent, and monospace code. Keep the error text readable: avoid red-on-red, a tiny caret, or a bare position 0 with no unit. Provide buttons for "Jump to position," "Copy raw error," and "Show raw input." When the parser reports only line and column, compute and display the character offset. When it reports only an offset, compute and display line and column. Always label which numbers start at 1 and which start at 0, because the labels are what prevent a user from pasting an offset into the wrong tool.

Two policy notices belong beside the syntax panel, not inside it. Duplicate keys and numbers outside the safe integer range are not grammar errors, but they can change meaning. This is an escentmachine policy recommendation: show a separate notice with the exact path and value, and do not silently keep the last duplicate or round a large integer.

The editorial rule is the same as the product rule: Inspect data. Find the mismatch. Show location, reason, and next step. Let the reader see the exact character that made the parser stop, and let them decide what to change.

Previous note
Next note