春江暮客

春江暮客的个人学习分享网站

Validate JSONL with Python: Find Broken Lines and Duplicate Keys

2026-10-14 Technology
Validate JSONL with Python: Find Broken Lines and Duplicate Keys

A batch job fails halfway through with a JSON error, but which line is broken? Calling json.load() on a whole JSONL file can also produce Extra data: the file intentionally contains several independent values.

This beginner-to-intermediate tutorial checks completed JSONL input with the Python standard library before starting another task. The tool locates errors by line, leaves the input unchanged, and prints a successful JSON report only after a complete read.

1. Method: validate physical lines before running the task

The JSON Lines format description requires UTF-8, one JSON value per line, and no BOM. Blank lines are invalid; null, arrays, and numbers can be records. A final newline is optional, and CRLF is supported.

This tool also rejects duplicate keys within objects and offers --objects for tasks that accept only dictionary-like records. These are tutorial policies: JSONL itself does not require object records. The default limit is 1 MiB per line, including the LF or CRLF bytes.

Forced termination can truncate the last record. Use the Python SIGTERM shutdown tutorial to handle the writer. Here we check completed ordinary local files that remain unchanged throughout validation.

2. Prepare the environment and test data

Use Python 3.9 or newer; no third-party packages are needed. Commands work in bash or zsh, and the examples were verified on Linux with Python 3.11.4.

  1. Create a new directory and keep subsequent commands in the same terminal and directory. Choose a different directory name when repeating the experiment:
python3 --version
mkdir jsonl-audit-demo
cd jsonl-audit-demo
printf '{"id":1,"name":"alpha"}\n{"id":2,"name":"beta"}\n' > good.jsonl
printf '{"id":1}\n{"id":}\n' > broken.jsonl
  1. Save the next section as audit_jsonl.py, or download the complete validation script.

3. Complete implementation

"""Validate completed UTF-8 JSONL input without changing it. Python 3.9+."""
import argparse
import json
from pathlib import Path
import sys


def line_limit(value):
    number = int(value)
    if not 1 <= number <= 16 * 1024 * 1024:
        raise argparse.ArgumentTypeError("max-line-bytes must be 1..16777216")
    return number


def reject_constant(value):
    raise ValueError(f"non-JSON number token: {value}")


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError(f"duplicate object key: {key!r}")
        result[key] = value
    return result


def audit(path, max_line_bytes, objects):
    records = 0
    with path.open("rb") as file:
        while True:
            raw = file.readline(max_line_bytes + 1)
            if not raw:
                break
            number = records + 1
            try:
                if len(raw) > max_line_bytes:
                    raise ValueError(f"line exceeds {max_line_bytes} bytes")
                text = raw.decode("utf-8", errors="strict")
                if not text.strip():
                    raise ValueError("blank line is not a JSON value")
                value = json.loads(
                    text,
                    parse_constant=reject_constant,
                    object_pairs_hook=unique_object,
                )
                if objects and not isinstance(value, dict):
                    raise ValueError("expected a JSON object")
            except json.JSONDecodeError as exc:
                raise ValueError(
                    f"line {number}, column {exc.colno}: {exc.msg}"
                ) from exc
            except (ValueError, RecursionError) as exc:
                raise ValueError(f"line {number}: {exc}") from exc
            records += 1
    return {"file": str(path), "records": records, "valid": True}


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("file", type=Path, help="completed, unchanging JSONL file")
    parser.add_argument("--objects", action="store_true", help="require object records")
    parser.add_argument(
        "--max-line-bytes", type=line_limit, default=1024 * 1024,
        help="physical line byte limit, including LF/CRLF (default: 1048576)",
    )
    args = parser.parse_args()
    try:
        result = audit(args.file, args.max_line_bytes, args.objects)
    except (OSError, ValueError) as exc:
        print(f"ERROR: {args.file}: {exc}", file=sys.stderr)
        return 1
    print(json.dumps(result, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

The official Python json documentation describes accepting extensions such as NaN and keeping the last value of repeated keys by default. We reject these using parse_constant and object_pairs_hook, including duplicate keys in nested objects.

The binary readline(size) documentation describes bounded reads. Reading limit + 1 bytes detects oversized lines before parsing, without loading the entire file first.

The tool stops at the first problem. Syntax errors show the physical line and JSON character column; encoding, duplicate-key, and length errors show the physical line. It does not validate business fields, IDs across records, or numeric ranges. Parsing one record still uses memory, and a byte limit does not provide complete resource isolation.

4. Run and verify a successful result

  1. Display help and check object records:
python3 audit_jsonl.py --help
python3 audit_jsonl.py good.jsonl --objects > report.json
cat report.json

Expected report:

{"file": "good.jsonl", "records": 2, "valid": true}
  1. Assert the actual content:
python3 - <<'PY_CHECK'
import json
from pathlib import Path
report = json.loads(Path("report.json").read_text(encoding="utf-8"))
assert report == {"file": "good.jsonl", "records": 2, "valid": True}
print("OK: two valid records")
PY_CHECK

Default mode allows values such as null and arrays; only --objects requires objects. An empty file reports zero valid records. If your task requires input, also check records > 0 in the caller.

For similar batch work in the streaming CSV tutorial, use the same pattern: check one record at a time and summarize at completion, without retaining every record.

5. Verify failure exits and gate the next task

  1. Check the broken line and read the exit status immediately. Run this interactively; scripts using set -e should put expected failures inside an if condition:
python3 audit_jsonl.py broken.jsonl --objects > failed-report.json
printf 'exit=%s\n' "$?"
wc -c < failed-report.json

Expect stderr to contain line 2, column 7, exit status 1, and a zero-byte failed-report.json. Redirection still creates or truncates the destination, so do not point it at an important existing report.

  1. Start the next task only after validation succeeds. A short print command represents the downstream task here:
if python3 audit_jsonl.py good.jsonl --objects; then
  python3 -c 'print("NEXT: input accepted")'
else
  printf 'STOP: invalid input\n' >&2
fi

Changing good.jsonl to broken.jsonl takes the failure branch and never prints NEXT. A real task must read the same unchanged input; validate again if the file changes afterward.

  1. Check the additional rules and boundaries:
printf '{"id":1,"id":2}\n' > duplicate.jsonl
printf '{"score":NaN}\n' > nonfinite.jsonl
printf '{}\n{}' > no-final-lf.jsonl
python3 audit_jsonl.py duplicate.jsonl
printf 'exit=%s\n' "$?"
python3 audit_jsonl.py nonfinite.jsonl
printf 'exit=%s\n' "$?"
python3 audit_jsonl.py no-final-lf.jsonl --objects
python3 audit_jsonl.py good.jsonl --max-line-bytes 8
printf 'exit=%s\n' "$?"

The first two checks report duplicate object key and non-JSON number token, each returning 1. The file without a final newline still reports two records. The last check returns 1 because the first line exceeds eight bytes. Success returns 0; invalid CLI arguments return 2.

Validation also covered Chinese UTF-8, invalid UTF-8, BOM, blank lines, CRLF, truncation, two values on one line, nested duplicate keys, deep nesting, missing files, and records exactly at the byte limit. Tests checked exit status, stdout, stderr, and unchanged input content.

6. Troubleshooting

Symptom Concrete fix
Extra data Write exactly one JSON value per line; re-export multiline pretty-printed JSON as JSONL.
blank line Check for extra blank lines from the generator and fix the writer instead of silently skipping records.
Unexpected UTF-8 BOM or decoding errors Re-export BOM-free UTF-8 from the source tool; ignoring decoding errors hides damaged data.
duplicate object key Fix repeated fields in the generator; this check does not establish unique IDs across records.
non-JSON number token Use json.dumps(record, allow_nan=False) in the writer and handle missing values first.
expected a JSON object Remove --objects if the task accepts arbitrary JSON values; otherwise fix the record structure.
line exceeds Check whether several lines were joined; for legitimate large records, use --max-line-bytes 2097152, up to 16 MiB.
Syntax error on the final line Check for interrupted copying or writing, and regenerate the complete file from its source.

If a scheduled run cannot find its input, follow Linux scheduled-job path troubleshooting and use absolute interpreter, script, and input paths.

Summary

Check UTF-8, each JSON line, and the required object policy before letting the exit status gate a batch job. Keep the error location and original file, fix the writer, and validate again. After validation, still check business fields and ensure the task reads unchanged input.

标签

1024 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena copy cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo imports indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 phaser pillow pip postfix preprocessing print probability protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode usdt utf-8 uv vhhbert vite wallet webp wordcloud wordpress workflow yaml zipfile zoneinfo 迅雷解析

友情链接

其它