On this page
Establish the file contract before parsing
The source dates shown in this guide identify documentation checks on 22 October 2026, not original publication dates. Follow each citation for the standard or documentation edition being discussed.
The jsonlines.org, (opens in a new tab) describes UTF-8, one valid JSON value on each line, and LF line terminators. CRLF is accepted because JSON whitespace can surround the value. A final line terminator is recommended but not required, a blank line is not a JSON value, and a byte order mark must not be included. The .jsonl extension is a convention, not a semantic guarantee.
| Contract item | Record before the first row is interpreted | Open question |
|---|---|---|
| Byte source | Relative artifact identifier, byte size, and digest or controlled reference | Is this the exact artifact named by the review? |
| Text rule | UTF-8 expectation, BOM policy, and compressed wrapper | Was the wrapper separated without changing the source? |
| Line rule | LF, accepted CRLF, final-newline policy, and blank-line policy | What does the producer treat as a record boundary? |
| Record rule | Object per line, or an explicit allowance for other JSON values | Is this dataset contract narrower than JSON Lines? |
| Schema rule | Required keys, allowed types, version field, and unknown-key policy | Which producer schema governs the review? |
| Identity rule | Field or combination identifying a record, or explicit unknown | Can duplicate identifiers be detected? |
| Failure rule | Stop, quarantine, or reconcile policy for a bad line | Who decides whether a derivative is allowed? |
Compression is a transport or packaging decision, not a change to the record grammar. If the delivered artifact is sample.jsonl.gz, retain the compressed-file digest and record the decompressed review artifact separately. Do not call a decompressed derivative the original file.
Parse one physical line at a time with bounded diagnostics
The useful diagnostic unit is the physical line, not an arbitrarily sampled JSON object. Keep the line number, stable issue class, and source artifact reference. Do not store a raw line, key, value, or parser message in a public report when it may contain a name, token, location, or confidential text.
Bounded JSON Lines helpers
import json
import math
from pathlib import Path
class RecordIssue(ValueError):
def __init__(self, kind: str):
super().__init__(kind)
self.kind = kind
def no_duplicate_names(pairs):
record = {}
for key, value in pairs:
if key in record:
raise RecordIssue("duplicate_key")
record[key] = value
return record
def positive_limit(value, name):
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
raise RecordIssue("invalid_limit")
return value
def reject_constant(_value: str):
raise RecordIssue("non_finite_number")
def bounded_int(value: str):
try:
return int(value)
except (OverflowError, ValueError):
raise RecordIssue("numeric_limit")
def finite_float(value: str):
try:
number = float(value)
except (OverflowError, ValueError):
raise RecordIssue("numeric_limit")
if not math.isfinite(number):
raise RecordIssue("non_finite_number")
return number
Pre-parse nesting guard
def scan_depth(text: str, max_depth: int):
stack = []
quoted = False
escaped = False
for character in text:
if quoted:
if escaped:
escaped = False
elif character == "\\":
escaped = True
elif character == '"':
quoted = False
continue
if character == '"':
quoted = True
elif character in "[{":
stack.append(']' if character == '[' else '}')
if len(stack) > max_depth:
raise RecordIssue("nesting_limit")
elif character in "]}":
if not stack or stack.pop() != character:
raise RecordIssue("invalid_nesting")
return len(stack)
Bounded line inspection
def inspect_line(raw_line: bytes, line_number: int, max_depth: int):
if raw_line.endswith(b"\r") and not raw_line.endswith(b"\r\n"):
return {"line": line_number, "kind": "invalid_line_ending"}
if line_number == 1 and raw_line.startswith(b"\xef\xbb\xbf"):
return {"line": line_number, "kind": "bom"}
if raw_line.strip() == b"":
return {"line": line_number, "kind": "blank_line"}
try:
text = raw_line.decode("utf-8", errors="strict")
scan_depth(text, max_depth)
value = json.loads(text, object_pairs_hook=no_duplicate_names,
parse_constant=reject_constant,
parse_int=bounded_int, parse_float=finite_float)
if not isinstance(value, dict):
return {"line": line_number, "kind": "record_not_object"}
except UnicodeDecodeError:
return {"line": line_number, "kind": "invalid_utf8"}
except RecordIssue as error:
return {"line": line_number, "kind": error.kind}
except RecursionError:
return {"line": line_number, "kind": "resource_limit"}
except json.JSONDecodeError as error:
return {"line": line_number, "kind": "invalid_json", "char_offset": error.pos}
return None
Bounded JSON Lines inspection
def inspect_jsonl(path: Path, max_line_bytes=1_000_000, max_issues=100,
max_depth=100):
max_line_bytes = positive_limit(max_line_bytes, "max_line_bytes")
max_issues = positive_limit(max_issues, "max_issues")
max_depth = positive_limit(max_depth, "max_depth")
issues = []
def add(issue):
issues.append(issue)
return len(issues) >= max_issues
with path.open("rb") as stream:
line_number = 0
while True:
raw_line = stream.readline(max_line_bytes + 1)
if raw_line == b"":
break
line_number += 1
if len(raw_line) > max_line_bytes:
while raw_line and not raw_line.endswith((b"\n", b"\r\n")):
raw_line = stream.readline(max_line_bytes + 1)
if add({"line": line_number, "kind": "line_too_long"}):
return issues
continue
issue = inspect_line(raw_line, line_number, max_depth)
if issue is not None and add(issue):
return issues
return issuesThe helper, depth guard, line, and main blocks form one bounded example. All three limits must be positive integers, and booleans are rejected explicitly because Python treats them as integers. The numeric hooks turn an integer-conversion limit into numeric_limit and reject ordinary exponential overflow as non_finite_number; parse_constant handles named non-finite constants. The raw line check rejects a bare carriage return while allowing LF, CRLF, and an unterminated final JSON value. The scanner tracks brackets outside escaped quoted strings before parsing, so a deeply nested document can receive a stable nesting_limit issue and a bracket in a string is not treated as structure. RecursionError becomes resource_limit without exception text. The example reads at most max_line_bytes plus one byte, drains an oversized physical line before continuing, and stops at max_issues. It emits only stable issue kinds and offsets, never a raw key, value, or parser error string. This is not a hostile-input sandbox; external CPU, memory, and process-time limits are still needed.
Synthetic edge cases
synthetic-nan -> non_finite_number
synthetic-infinity -> non_finite_number
synthetic-duplicate -> duplicate_key
nan -> non_finite_number
infinity -> non_finite_number
nested_duplicate -> duplicate_key
very_large_integer -> numeric_limit
exponential_overflow -> non_finite_number
blank_whitespace -> blank_line
nonobject -> record_not_object
invalid_utf8 -> invalid_utf8
bare_carriage_return -> invalid_line_ending
oversized_line -> line_too_long
deep_nesting -> nesting_limit
brackets_inside_string -> no_nesting_issue
max_line_bytes_negative -> invalid_limit
max_issues_zero -> invalid_limit
max_depth_bool -> invalid_limitThe synthetic cases cover non-finite numbers, duplicate names, whitespace-only input, a scalar where this dataset contract requires an object, invalid UTF-8, an oversized physical line, deep nesting, a bracket sequence inside a quoted string, and explicit negative, zero, or boolean limit values for the three bounded controls. Keep fixtures synthetic and bounded. Do not use real names, messages, addresses, or locations to demonstrate that the parser ran.
Apply record semantics only after syntax passes
JSON Lines permits any JSON value on a line. A dataset may choose a narrower contract, such as one object with a string identifier and an integer schema version. That choice belongs in the dataset contract, not in the format name. RFC 8259 also recommends unique object member names because duplicate names can make receiver behavior unpredictable Internet Engineering Task Force, (opens in a new tab).
Synthetic record fixture
{
"record_id": "synthetic-001",
"schema_version": 1,
"event": "created",
"occurred_at": "2026-01-02T03:04:05Z"
}| Field | Synthetic contract | Review question |
|---|---|---|
| record_id | Non-empty string, unique within the declared export | Is uniqueness tested, or only assumed? |
| schema_version | Integer selected by the producer | Does a reader know which rules apply? |
| event | One value from a documented codebook | Are unknown labels held for review? |
| occurred_at | RFC-style instant or explicit null state | Does null mean unavailable, not applicable, or error? |
Do not silently coerce a string "1" to an integer, an empty string to null, or a missing key to a default unless the contract names that transformation. A normalized derivative needs its own artifact reference and digest.
Use a disposition table for failures
| Observation | Safe interpretation | Next action |
|---|---|---|
| First bytes contain a BOM | The source is not in the stated JSON Lines byte form | Preserve the source and decide whether a documented derivative is allowed |
| Blank or whitespace-only physical line | No JSON value exists on that line | Reconcile the producer rule; do not count it as a record |
| UTF-8 decode failure | The declared text contract is not satisfied there | Ask for the source encoding or corrected export |
| JSON parse failure | The line is not one valid JSON value under the parser | Keep line number, stable class, and safe offset; do not repair silently |
| Valid scalar where an object is required | Format syntax passed but dataset shape failed | Record a schema-shape failure |
| Duplicate object name | The same member name occurs more than once | Preserve the line and require an owner decision |
| NaN or Infinity constant | A non-standard numeric constant was presented | Reject it or document a producer-specific contract |
| Physical line exceeds the configured limit | Review policy cannot safely process the line | Record line_too_long and stop or quarantine it |
| Object parses but required key is missing | Syntax passed; record contract failed | Classify missingness and compare the producer schema |
| All checks pass | The selected format and shape checks have evidence | Continue provenance, privacy, rights, and accuracy review |
Do not collapse findings into a single “clean” score. Valid syntax can coexist with incomplete vocabulary, duplicate identifiers, or changed field meaning. Keep each finding in a separate column so a later reader can tell what was checked.
Keep diagnostics safe and the next step qualified
For an export that may contain sensitive records, a public worksheet can contain the file reference, line number, issue class, schema version, and stable evidence reference. It should not copy a message, token, address, precise location, or raw row merely to prove that parsing ran. If a redacted excerpt is needed, record approval, the transformation, and remaining uncertainty.
Pair this check with the data provenance log and broader export guidance. The provenance log owns source, rights, processing, and exclusion evidence. Export guidance owns tool-specific quality. This guide owns line-level JSON diagnostics.
After the owner has a supported non-health scope, authority to describe the collection, and a metadata or synthetic review packet, the qualified appraisal request path may be a next preparation step. It does not validate JSON Lines, accept a dataset, create a quote, match a buyer, issue a legal opinion, or grant a license. Keep raw sensitive records outside this guide and within the owner-approved handling boundary.
Takeaways
- Treat UTF-8, one value per physical line, newline behavior, and BOM handling as explicit contract items.
- Bound line size and issue count, and emit stable diagnostics without raw keys, values, or parser messages.
- Parse and report line failures before deciding whether records satisfy a dataset schema.
- Use synthetic or metadata-only fixtures before exposing raw records.
- Keep syntax, schema, provenance, privacy, rights, accuracy, and demand as separate questions.