Skip to content

Commit 30a5d47

Browse files
authored
Merge pull request #154 from jamdotdev/petar/jsonl-validator
feat: create jsonl-validator
2 parents af3f714 + b9d8514 commit 30a5d47

File tree

6 files changed

+660
-0
lines changed

6 files changed

+660
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Here is the list of all utilities:
5353
- [Internet Speed Test](https://jam.dev/utilities/internet-speed-test)
5454
- [Random String Generator](https://jam.dev/utilities/random-string-generator)
5555
- [CSV file viewer](https://jam.dev/utilities/csv-file-viewer)
56+
- [JSONL Validator](https://jam.dev/utilities/jsonl-validator)
5657

5758
### Built With
5859

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import Link from "next/link";
2+
3+
export default function JsonlValidatorSEO() {
4+
return (
5+
<div className="content-wrapper">
6+
<section>
7+
<p>
8+
JSONL (JSON Lines) is a backbone format for AI pipelines,
9+
observability exports, and event-driven backends. Use this validator
10+
to catch bad rows fast, jump to exact error lines, and export clean
11+
records as a JSON array.
12+
</p>
13+
</section>
14+
15+
<section>
16+
<h2>How to use this JSONL validator</h2>
17+
<ul>
18+
<li>
19+
Paste JSONL directly from logs, S3 dumps, Kafka consumers, or AI
20+
dataset files. Each line should be one valid JSON value.
21+
</li>
22+
<li>
23+
Review line-level issues with line and column hints, then jump to
24+
the exact row to fix malformed entries quickly.
25+
</li>
26+
<li>
27+
Copy valid rows as a JSON array for local scripts, backfills, smoke
28+
tests, or one-off API replay jobs.
29+
</li>
30+
</ul>
31+
</section>
32+
33+
<section>
34+
<h2>Built for modern developer workflows</h2>
35+
<ul>
36+
<li>
37+
<b>AI and LLM datasets:</b> <br /> Validate training samples, eval
38+
traces, and prompt/response logs before running expensive jobs.
39+
</li>
40+
<li>
41+
<b>Observability and incident response:</b> <br /> Triage broken log
42+
lines quickly when debugging production telemetry.
43+
</li>
44+
<li>
45+
<b>Data pipeline reliability:</b> <br /> Clean malformed events
46+
before loading to warehouses or replaying through queues.
47+
</li>
48+
</ul>
49+
</section>
50+
51+
<section>
52+
<h2>Why this validator is useful</h2>
53+
<ul>
54+
<li>
55+
<b>Line-by-line diagnostics:</b> <br /> Find exactly where parsing
56+
fails instead of guessing across large payloads.
57+
</li>
58+
<li>
59+
<b>Developer-first speed:</b> <br /> Designed for quick iteration
60+
when you are fixing data during debugging sessions.
61+
</li>
62+
<li>
63+
<b>Client-side workflow:</b> <br /> Run checks in-browser without
64+
sending payloads to third-party servers.
65+
</li>
66+
</ul>
67+
</section>
68+
69+
<section>
70+
<h2>Related tools</h2>
71+
<ul>
72+
<li>
73+
<Link href="/utilities/json-formatter">JSON Formatter</Link>: Format
74+
and validate JSON for readability.
75+
</li>
76+
<li>
77+
<Link href="/utilities/csv-to-json">CSV to JSON</Link>: Convert
78+
tabular data into JSON records.
79+
</li>
80+
<li>
81+
<Link href="/utilities/json-to-csv">JSON to CSV</Link>: Turn JSON
82+
arrays into spreadsheet-ready CSV files.
83+
</li>
84+
</ul>
85+
</section>
86+
</div>
87+
);
88+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { parseJsonLines, toJsonArrayString } from "./jsonl-validator.utils";
2+
3+
describe("jsonl-validator.utils", () => {
4+
describe("parseJsonLines", () => {
5+
it("returns an empty result for empty input", () => {
6+
expect(parseJsonLines("")).toEqual({
7+
totalLines: 0,
8+
emptyLines: 0,
9+
validLines: 0,
10+
invalidLines: 0,
11+
records: [],
12+
errors: [],
13+
keyFrequency: {},
14+
});
15+
});
16+
17+
it("parses valid JSONL lines and computes key frequency", () => {
18+
const input = [
19+
'{"id":1,"level":"info","message":"ok"}',
20+
'{"id":2,"level":"warn"}',
21+
'{"id":3,"level":"error","code":"E_TIMEOUT"}',
22+
].join("\n");
23+
24+
const result = parseJsonLines(input);
25+
26+
expect(result.totalLines).toBe(3);
27+
expect(result.emptyLines).toBe(0);
28+
expect(result.validLines).toBe(3);
29+
expect(result.invalidLines).toBe(0);
30+
expect(result.records).toHaveLength(3);
31+
expect(result.keyFrequency).toEqual({
32+
id: 3,
33+
level: 3,
34+
message: 1,
35+
code: 1,
36+
});
37+
});
38+
39+
it("ignores empty lines and reports invalid lines with line numbers", () => {
40+
const input = ['{"ok": true}', "", "{", "not-json", '{"ok": false}'].join(
41+
"\n"
42+
);
43+
44+
const result = parseJsonLines(input);
45+
46+
expect(result.totalLines).toBe(4);
47+
expect(result.emptyLines).toBe(1);
48+
expect(result.validLines).toBe(2);
49+
expect(result.invalidLines).toBe(2);
50+
expect(result.errors[0].lineNumber).toBe(3);
51+
expect(result.errors[0].lineContent).toBe("{");
52+
expect(result.errors[0].columnNumber).toBeGreaterThan(0);
53+
expect(result.errors[1].lineNumber).toBe(4);
54+
expect(result.errors[1].lineContent).toBe("not-json");
55+
expect(result.errors[1].columnNumber).toBeUndefined();
56+
});
57+
58+
it("accepts non-object JSON values as valid records", () => {
59+
const input = ['"text"', "42", "true", "null", "[1,2,3]"].join("\n");
60+
61+
const result = parseJsonLines(input);
62+
63+
expect(result.validLines).toBe(5);
64+
expect(result.invalidLines).toBe(0);
65+
expect(result.keyFrequency).toEqual({});
66+
});
67+
});
68+
69+
describe("toJsonArrayString", () => {
70+
it("formats records as pretty-printed JSON array", () => {
71+
const output = toJsonArrayString([{ id: 1 }, { id: 2 }]);
72+
expect(output).toBe(
73+
'[\n {\n "id": 1\n },\n {\n "id": 2\n }\n]'
74+
);
75+
});
76+
});
77+
});
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
export type JsonlParseError = {
2+
lineNumber: number;
3+
columnNumber?: number;
4+
message: string;
5+
lineContent: string;
6+
};
7+
8+
export type JsonlValidationResult = {
9+
totalLines: number;
10+
emptyLines: number;
11+
validLines: number;
12+
invalidLines: number;
13+
records: unknown[];
14+
errors: JsonlParseError[];
15+
keyFrequency: Record<string, number>;
16+
};
17+
18+
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
19+
return typeof value === "object" && value !== null && !Array.isArray(value);
20+
};
21+
22+
const extractColumnNumber = (message: string) => {
23+
const columnMatch = message.match(/column\s+(\d+)/i);
24+
if (columnMatch) {
25+
const parsedColumn = Number(columnMatch[1]);
26+
return Number.isNaN(parsedColumn) ? undefined : parsedColumn;
27+
}
28+
29+
const positionMatch = message.match(/position\s+(\d+)/i);
30+
if (positionMatch) {
31+
const parsedPosition = Number(positionMatch[1]);
32+
if (!Number.isNaN(parsedPosition)) {
33+
return parsedPosition + 1;
34+
}
35+
}
36+
37+
return undefined;
38+
};
39+
40+
export const parseJsonLines = (input: string): JsonlValidationResult => {
41+
if (input.trim() === "") {
42+
return {
43+
totalLines: 0,
44+
emptyLines: 0,
45+
validLines: 0,
46+
invalidLines: 0,
47+
records: [],
48+
errors: [],
49+
keyFrequency: {},
50+
};
51+
}
52+
53+
const lines = input.split(/\r?\n/);
54+
const records: unknown[] = [];
55+
const errors: JsonlParseError[] = [];
56+
const keyFrequency: Record<string, number> = {};
57+
let emptyLines = 0;
58+
59+
lines.forEach((line, index) => {
60+
const trimmedLine = line.trim();
61+
62+
if (trimmedLine === "") {
63+
emptyLines += 1;
64+
return;
65+
}
66+
67+
try {
68+
const parsed = JSON.parse(trimmedLine);
69+
records.push(parsed);
70+
71+
if (isPlainObject(parsed)) {
72+
Object.keys(parsed).forEach((key) => {
73+
keyFrequency[key] = (keyFrequency[key] || 0) + 1;
74+
});
75+
}
76+
} catch (error) {
77+
const message = error instanceof Error ? error.message : "Invalid JSON";
78+
errors.push({
79+
lineNumber: index + 1,
80+
columnNumber: extractColumnNumber(message),
81+
message,
82+
lineContent: line,
83+
});
84+
}
85+
});
86+
87+
const totalLines = lines.length - emptyLines;
88+
89+
return {
90+
totalLines,
91+
emptyLines,
92+
validLines: records.length,
93+
invalidLines: errors.length,
94+
records,
95+
errors,
96+
keyFrequency,
97+
};
98+
};
99+
100+
export const toJsonArrayString = (records: unknown[]) => {
101+
return JSON.stringify(records, null, 2);
102+
};

components/utils/tools-list.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export const tools = [
1717
"Format and beautify your JSON data for better readability and debugging. Quickly visualize and organize your JSON data with ease.",
1818
link: "/utilities/json-formatter",
1919
},
20+
{
21+
title: "JSONL Validator",
22+
description:
23+
"Validate JSON Lines instantly, find broken rows by line number, and convert valid records to a clean JSON array.",
24+
link: "/utilities/jsonl-validator",
25+
},
2026
{
2127
title: "YAML to JSON",
2228
description:

0 commit comments

Comments
 (0)