Batu Lab NotesPractical developer guides

Report the first invalid JSON Lines record number

By Batu · English technical notes

To report the first invalid JSON Lines record number, iterate over the text stream with enumerate(..., start=1) and call json.loads() for each line. A whole-text json.loads() call only sees one JSON document; with this fixture, it reports Extra data after the first valid object instead of identifying the malformed second record. json.loads raises JSONDecodeError for invalid JSON, and that exception includes parsing-location attributes, but the stream loop supplies the record number directly.

The fixture deliberately contains valid, malformed, valid records. First, the code shows the unhelpful whole-file error. Next, it starts again with StringIO, accepts record 1, encounters {"id": } on record 2, prints that record number, and breaks. The final list demonstrates that record 3 was not decoded after the first error.

This is a fail-fast validator, not an error collector. Blank lines would also cause json.loads() to fail unless the code defines a separate blank-line policy. Likewise, “record number” here means physical input line number, so multi-line JSON values do not fit this simple JSON Lines contract. JSONDecodeError was added in Python 3.5; no newer API is required.

AI assistance disclosure: the example was AI-assisted and executed only against the shown in-memory fixture.

Example

import json
from io import StringIO

text = '{"id": 1}\n{"id": }\n{"id": 3}\n'
print(repr(text))

try:
    json.loads(text)
except json.JSONDecodeError as error:
    print(f"whole-file error: {error.msg}")

accepted = []
for record_number, line in enumerate(StringIO(text), start=1):
    try:
        accepted.append(json.loads(line))
    except json.JSONDecodeError:
        print(f"first invalid record: {record_number}")
        break

assert accepted == [{"id": 1}]
print(accepted)

Expected output:

'{"id": 1}\n{"id": }\n{"id": 3}\n'
whole-file error: Extra data
first invalid record: 2
[{'id': 1}]