Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Here is the list of all utilities:
- [Internet Speed Test](https://jam.dev/utilities/internet-speed-test)
- [Random String Generator](https://jam.dev/utilities/random-string-generator)
- [CSV file viewer](https://jam.dev/utilities/csv-file-viewer)
- [JSONL Validator](https://jam.dev/utilities/jsonl-validator)

### Built With

Expand Down
88 changes: 88 additions & 0 deletions components/seo/JsonlValidatorSEO.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Link from "next/link";

export default function JsonlValidatorSEO() {
return (
<div className="content-wrapper">
<section>
<p>
JSONL (JSON Lines) is a backbone format for AI pipelines,
observability exports, and event-driven backends. Use this validator
to catch bad rows fast, jump to exact error lines, and export clean
records as a JSON array.
</p>
</section>

<section>
<h2>How to use this JSONL validator</h2>
<ul>
<li>
Paste JSONL directly from logs, S3 dumps, Kafka consumers, or AI
dataset files. Each line should be one valid JSON value.
</li>
<li>
Review line-level issues with line and column hints, then jump to
the exact row to fix malformed entries quickly.
</li>
<li>
Copy valid rows as a JSON array for local scripts, backfills, smoke
tests, or one-off API replay jobs.
</li>
</ul>
</section>

<section>
<h2>Built for modern developer workflows</h2>
<ul>
<li>
<b>AI and LLM datasets:</b> <br /> Validate training samples, eval
traces, and prompt/response logs before running expensive jobs.
</li>
<li>
<b>Observability and incident response:</b> <br /> Triage broken log
lines quickly when debugging production telemetry.
</li>
<li>
<b>Data pipeline reliability:</b> <br /> Clean malformed events
before loading to warehouses or replaying through queues.
</li>
</ul>
</section>

<section>
<h2>Why this validator is useful</h2>
<ul>
<li>
<b>Line-by-line diagnostics:</b> <br /> Find exactly where parsing
fails instead of guessing across large payloads.
</li>
<li>
<b>Developer-first speed:</b> <br /> Designed for quick iteration
when you are fixing data during debugging sessions.
</li>
<li>
<b>Client-side workflow:</b> <br /> Run checks in-browser without
sending payloads to third-party servers.
</li>
</ul>
</section>

<section>
<h2>Related tools</h2>
<ul>
<li>
<Link href="/utilities/json-formatter">JSON Formatter</Link>: Format
and validate JSON for readability.
</li>
<li>
<Link href="/utilities/csv-to-json">CSV to JSON</Link>: Convert
tabular data into JSON records.
</li>
<li>
<Link href="/utilities/json-to-csv">JSON to CSV</Link>: Turn JSON
arrays into spreadsheet-ready CSV files.
</li>
</ul>
</section>
</div>
);
}
77 changes: 77 additions & 0 deletions components/utils/jsonl-validator.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { parseJsonLines, toJsonArrayString } from "./jsonl-validator.utils";

describe("jsonl-validator.utils", () => {
describe("parseJsonLines", () => {
it("returns an empty result for empty input", () => {
expect(parseJsonLines("")).toEqual({
totalLines: 0,
emptyLines: 0,
validLines: 0,
invalidLines: 0,
records: [],
errors: [],
keyFrequency: {},
});
});

it("parses valid JSONL lines and computes key frequency", () => {
const input = [
'{"id":1,"level":"info","message":"ok"}',
'{"id":2,"level":"warn"}',
'{"id":3,"level":"error","code":"E_TIMEOUT"}',
].join("\n");

const result = parseJsonLines(input);

expect(result.totalLines).toBe(3);
expect(result.emptyLines).toBe(0);
expect(result.validLines).toBe(3);
expect(result.invalidLines).toBe(0);
expect(result.records).toHaveLength(3);
expect(result.keyFrequency).toEqual({
id: 3,
level: 3,
message: 1,
code: 1,
});
});

it("ignores empty lines and reports invalid lines with line numbers", () => {
const input = ['{"ok": true}', "", "{", "not-json", '{"ok": false}'].join(
"\n"
);

const result = parseJsonLines(input);

expect(result.totalLines).toBe(4);
expect(result.emptyLines).toBe(1);
expect(result.validLines).toBe(2);
expect(result.invalidLines).toBe(2);
expect(result.errors[0].lineNumber).toBe(3);
expect(result.errors[0].lineContent).toBe("{");
expect(result.errors[0].columnNumber).toBeGreaterThan(0);
expect(result.errors[1].lineNumber).toBe(4);
expect(result.errors[1].lineContent).toBe("not-json");
expect(result.errors[1].columnNumber).toBeUndefined();
});

it("accepts non-object JSON values as valid records", () => {
const input = ['"text"', "42", "true", "null", "[1,2,3]"].join("\n");

const result = parseJsonLines(input);

expect(result.validLines).toBe(5);
expect(result.invalidLines).toBe(0);
expect(result.keyFrequency).toEqual({});
});
});

describe("toJsonArrayString", () => {
it("formats records as pretty-printed JSON array", () => {
const output = toJsonArrayString([{ id: 1 }, { id: 2 }]);
expect(output).toBe(
'[\n {\n "id": 1\n },\n {\n "id": 2\n }\n]'
);
});
});
});
102 changes: 102 additions & 0 deletions components/utils/jsonl-validator.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
export type JsonlParseError = {
lineNumber: number;
columnNumber?: number;
message: string;
lineContent: string;
};

export type JsonlValidationResult = {
totalLines: number;
emptyLines: number;
validLines: number;
invalidLines: number;
records: unknown[];
errors: JsonlParseError[];
keyFrequency: Record<string, number>;
};

const isPlainObject = (value: unknown): value is Record<string, unknown> => {
return typeof value === "object" && value !== null && !Array.isArray(value);
};

const extractColumnNumber = (message: string) => {
const columnMatch = message.match(/column\s+(\d+)/i);
if (columnMatch) {
const parsedColumn = Number(columnMatch[1]);
return Number.isNaN(parsedColumn) ? undefined : parsedColumn;
}

const positionMatch = message.match(/position\s+(\d+)/i);
if (positionMatch) {
const parsedPosition = Number(positionMatch[1]);
if (!Number.isNaN(parsedPosition)) {
return parsedPosition + 1;
}
}

return undefined;
};

export const parseJsonLines = (input: string): JsonlValidationResult => {
if (input.trim() === "") {
return {
totalLines: 0,
emptyLines: 0,
validLines: 0,
invalidLines: 0,
records: [],
errors: [],
keyFrequency: {},
};
}

const lines = input.split(/\r?\n/);
const records: unknown[] = [];
const errors: JsonlParseError[] = [];
const keyFrequency: Record<string, number> = {};
let emptyLines = 0;

lines.forEach((line, index) => {
const trimmedLine = line.trim();

if (trimmedLine === "") {
emptyLines += 1;
return;
}

try {
const parsed = JSON.parse(trimmedLine);
records.push(parsed);

if (isPlainObject(parsed)) {
Object.keys(parsed).forEach((key) => {
keyFrequency[key] = (keyFrequency[key] || 0) + 1;
});
}
} catch (error) {
const message = error instanceof Error ? error.message : "Invalid JSON";
errors.push({
lineNumber: index + 1,
columnNumber: extractColumnNumber(message),
message,
lineContent: line,
});
}
});

const totalLines = lines.length - emptyLines;

return {
totalLines,
emptyLines,
validLines: records.length,
invalidLines: errors.length,
records,
errors,
keyFrequency,
};
};

export const toJsonArrayString = (records: unknown[]) => {
return JSON.stringify(records, null, 2);
};
6 changes: 6 additions & 0 deletions components/utils/tools-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export const tools = [
"Format and beautify your JSON data for better readability and debugging. Quickly visualize and organize your JSON data with ease.",
link: "/utilities/json-formatter",
},
{
title: "JSONL Validator",
description:
"Validate JSON Lines instantly, find broken rows by line number, and convert valid records to a clean JSON array.",
link: "/utilities/jsonl-validator",
},
{
title: "YAML to JSON",
description:
Expand Down
Loading