Code Formatting

JSON Lines Validator

Validate and normalize JSONL/NDJSON line by line or convert it to a JSON array.

JSON Lines / NDJSON
Each non-empty line must contain one complete JSON value.
0Non-empty lines
0Valid lines
0Invalid lines
0Input characters
Validation and output
Output will appear here after validation.

JSON Lines Validation

JSONL, also called NDJSON, stores one complete JSON value per non-empty line. This tool reports line-level errors and produces normalized JSONL or a regular JSON array.

Boundaries

  • Blank lines are ignored
  • A single JSON value cannot span multiple lines
  • Object keys can be sorted recursively without reordering arrays

JSON Lines (JSONL, also called NDJSON) stores one complete JSON value per line so a reader can parse records one at a time. This page splits whatever you paste on line breaks, parses every non-blank line on its own, reports failing lines by number, and re-emits compact JSONL or a single JSON array.

The framing rules come from jsonlines.org, not an RFC

JSON Lines is defined by convention rather than by a standards document. jsonlines.org fixes three rules: UTF-8 encoding, exactly one valid JSON value per line, and U+000A as the separator. The NDJSON variant at ndjson.github.io adds that the file should end with a newline and that no BOM may appear. Because a newline ends the record, a value can never be printed across several lines: a break inside a string has to be written as the two characters \n, which is precisely what JSON.stringify emits. The alternative for texts that must stay multi-line is RFC 7464, JSON Text Sequences with media type application/json-seq, where each value is prefixed by U+001E instead. Pick JSONL when a producer appends one record per write and the consumer reads line by line.

Where the line splitter disagrees with your actual file

Input is split on the pattern \r?\n, so LF and CRLF files both work, but a file using lone CR (U+000D, classic Mac line ends, some serial or device captures) never splits and arrives as one enormous line that cannot parse. Whitespace-only lines are skipped before parsing, while error numbers are computed from the raw split index, so the reported line matches the one your editor shows, blank lines included. A byte order mark is stripped from line 1 only: concatenate three exports that each begin with U+FEFF and the second and third BOM end up inside a record, where the parser answers with an "Unexpected token" message quoting that invisible character. Pasting a pretty-printed object as a block therefore yields one error per line after the first brace.

Output is rebuilt per record, and only if every line parses

Validation collects values but publishes nothing until all of them parse: a single bad record leaves the output panel empty, and the invalid counter tells you how much is still broken. When everything parses, Normalize mode calls JSON.stringify on each value and joins the results with U+000A, so all original spacing vanishes and escapes are rewritten, control characters becoming \u0001-style sequences while CJK characters stay literal. Records survive only as data: keys that look like integers are hoisted in ascending order by JavaScript object semantics, so a record written with "10" before "2" prints in the opposite order. The optional key sort uses localeCompare, an ICU collation rather than byte order. To-array mode instead prints one JSON.stringify(values, null, 2) document.

Worked example

Normalizing three records that are separated by a blank line

Input
{"id":1, "name":"Ada"}

{"id":2,"name":"林","tags":["api","ops"]}
{"id":3,"name":"Sam","active":false}
Output
{"id":1,"name":"Ada"}
{"id":2,"name":"林","tags":["api","ops"]}
{"id":3,"name":"Sam","active":false}

The counters report 3 non-empty lines, 3 valid and 0 invalid, so the blank line is skipped rather than flagged. The space in {"id":1, "name"... disappears because each record is re-serialized, not re-indented, and 林 is written back as a literal character instead of the \u6797 escape. To-array mode wraps these same three values in brackets at two-space indentation.

Limits and pitfalls

  • Only the first 8 failing lines are printed, because the status block slices that list; the counter still reports the true total, so a heavily damaged file needs several repair passes.
  • No size or line cap is enforced: every line goes through JSON.parse and the result is rebuilt in one pass, while the "input characters" figure counts UTF-16 code units, so one emoji adds 2.
  • One invalid line empties the whole output, so the page cannot salvage the good records from a partly corrupted export.
  • Sorting is done with localeCompare collation: _z sorts before 10, and 10 before 2, unlike the byte order that sorting flags in jq or Python produce.
  • LF and CRLF are handled, but a CR-only file collapses into a single line, and a BOM on any line after the first is left in place and breaks that record.

Related tools

When it goes wrong

I pasted my data and the panel is silent: no green line, no error, and the Copy button stays greyed out.

CauseNothing reached the parser. Whitespace-only lines are skipped before JSON.parse runs, and the output is built by joining the parsed values — with zero values that join is the empty string, so analysis.output is falsy. The success line sits in the v-else-if that also requires non-blank input, so an all-blank paste produces no message whatsoever.

FixRead the Non-empty lines counter: 0 means the box holds only newlines, tabs or a BOM (the first line is trimmed before the BOM is even stripped). Select all and paste again from the source, or check that you did not open a binary file.

The counter claims 400 broken lines, but only eight of them are printed underneath.

CauseThe status block renders analysis.errors.slice(0, 8) while the counter prints the full analysis.errors.length. There is no paging, no expand control and no way to download the list.

FixRepair the eight visible lines and paste again: each pass clears at least the lines it showed you. Once the count runs into the hundreds, stop patching by hand and put a per-line script on it.

Line 1 passes and line 2 is rejected, even though I copied it out of the same source and it looks like valid JSON.

CauseOnly the first line has a BOM removed (index === 0 ? line.replace(/^\uFEFF/, "") : line). Concatenating three exports with cat keeps the BOM each file began with, so the second and third sit in front of a record where the parser cannot accept them, and its message quotes that invisible character.

FixDrop the BOM before pasting: for every file after the first, tail -c +4 skips the opening three bytes when you build the concatenation. Or paste each export into a clean box separately and concatenate the outputs instead of the inputs.

One line in a 5,000-line log reads request failed after 30s, and now the output panel is empty.

CauseEvery non-blank line goes through JSON.parse, and any single failure assigns an empty string to the output — the page never emits a partial result. A plain-text log line is not a JSON value, so it takes the other 4,999 records down with it.

FixFilter the input down to record lines first, e.g. grep "^{" app.log > records.jsonl, or hand a mixed log to a script with a per-line try. Text and JSON interleaved cannot be normalised on this page in one pass.

Pasting a few hundred thousand lines locks the tab up, and I cannot even finish typing.

CauseThe analysis is a Vue computed over the raw input string: every keystroke and every paste re-splits the entire text, calls JSON.parse on every line and re-serialises all collected values. There is no debounce, no chunking and no size cap, so the work scales with the whole document on each change.

FixPaste in slices of a few thousand lines and concatenate the outputs in order, or run jq -c . on whichever machine holds the file. Nothing here limits input size — the tab is the limit.

Validation came back green, but the importer downstream still rejects the rows.

CauseGreen means each line parsed as some JSON value, not that it parsed as an object with your fields. Lines holding 123, "text", true or [1,2] pass identically, and the valid counter counts them as records.

FixCheck shape before shipping: run the text through ajv or jsonschema with type: object. Without a validator, scan for lines that start with a digit, a quote or a bracket — those are scalar or array records that an object-expecting importer will choke on.

This page, jq, or the JSON formatter: which one the job in front of you actually needs

What you have in handjson-lines hereReal alternative
A .jsonl file already on disk, larger than you would ever pasteNo file input and no streaming: the whole document has to sit in one textarea, and parsing plus rebuilding runs as a single synchronous pass over that string, so this page is for samples and short exports.jq -c . export.jsonl > /dev/null reads one record at a time and never materialises the file. Switch as soon as the file is bigger than what you would paste into a box by hand.
A damaged export where you need the surviving records, not a diagnosisValidation is all-or-nothing: a single failing line sets the output to an empty string, so the 9,999 intact records never reach the copy button.A loop that parses each line inside try/except and writes what survives (ten lines of Python) recovers everything still parseable. Use this page to find out how bad the damage is; use the script to do the salvage.
One JSON document crushed onto a single line that you want to readNormalize calls JSON.stringify with no indent, so it compacts rather than expands; To array does indent, but only after wrapping every line into an array, which adds a bracket level you then have to strip again.The json-format tool, or jq ., indents a value as it stands at whatever width you ask for. Line-delimited input is the one shape those tools handle badly, which is precisely the gap this page fills.
Check that every record carries id, name and activeOnly syntax is checked. JSON.parse accepts any JSON value, so a line holding 42 or "ok" counts as valid, the green banner appears, and the fields you care about are still absent.ajv against a JSON Schema, or jsonschema under Python, checks types, required keys and formats. This page answers "is it JSON" and never "is it the right JSON".
Wrap JSONL into an array for a request bodyTo array prints JSON.stringify(values, null, 2): a two-space indent, no other setting, and the entire array assembled as one string in memory.jq -s . records.jsonl slurps just the same way but takes --indent 0 or --tab, and jq -c -s . yields the compact form a header or URL needs. Pick jq when the consumer is fussy about whitespace.
The paste turns out to be CSV or a tab-separated dumpEvery line fails from the header down, because a comma-separated row is not a JSON value at all. Non-empty lines and invalid lines showing the same number is the signature of a wrong-tool choice.The csv-json tool splits fields, honouring quotes and nested cells. Come back here once the producer emits one JSON object per line.

Frequently Asked Questions

JSON Lines vs a plain JSON array — what is the difference?
Each JSONL line is a complete, independent JSON value that can be parsed in a stream — the standard for logs, LLM training corpora, and batch exports. A JSON array must be fully loaded before parsing. JSONL also supports appending: add a line without rewriting the file.
Line N failed validation — how do I see why?
The tool parses line by line and reports the line number and cause (usual suspects: trailing commas, unescaped newlines inside strings, multiple objects on one line). Note a JSONL value cannot span lines — pretty-printed objects must be minified to a single line first.
How do I convert a JSON array to JSONL?
This tool converts both ways: each array element becomes one line. Many stream-processing tools (Spark, pandas streaming readers) only accept JSONL, so the conversion is a staple of data pipelines.

Content reviewed: