JSON Validator — TypeScript source
Validate JSON and pinpoint errors with line and column numbers. Clear valid/invalid verdict plus the exact error location - runs entirely in your browser.
This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Pure JSON validation logic. No React, no DOM. Deterministic and never throws.
//
// The browser's JSON engine (V8) embeds the error offset as "at position N" in
// its SyntaxError message; JavaScriptCore (bun) does not. Position recovery is
// therefore best-effort: when the offset is present it is converted to a 1-based
// line/column, otherwise line/column gracefully fall back to null.
export interface ValidationResult {
valid: boolean;
error: string | null;
line: number | null;
column: number | null;
}
/** Extract the 0-based char offset from a SyntaxError message (V8: "at position N"). Null when absent. */
export function extractOffset(message: string): number | null {
const match = /at position (\d+)/.exec(message);
return match ? Number(match[1]) : null;
}
/** Coerce a caught value into a human-readable message. */
export function errorMessage(value: unknown): string {
return value instanceof Error ? value.message : String(value);
}
/** Normalize an engine error message for display (strips JavaScriptCore's "JSON Parse error: " prefix). */
export function normalizeError(message: string): string {
return message.replace(/^JSON Parse error:\s*/i, '');
}
/**
* Convert a 0-based char offset into a 1-based { line, column }.
* Counts `\n`, a lone `\r`, and `\r\n` each as a single line break.
*/
export function offsetToLineCol(text: string, offset: number): { line: number; column: number } {
let line = 1;
let column = 1;
const max = Math.min(offset, text.length);
for (let i = 0; i < max; i++) {
const ch = text[i];
if (ch === '\n') {
line++;
column = 1;
} else if (ch === '\r') {
line++;
column = 1;
if (text[i + 1] === '\n') i++; // treat CRLF as a single break
} else {
column++;
}
}
return { line, column };
}
/** Map a parse error message to a { line, column } (or nulls) using the input text. */
export function locateError(text: string, message: string): Pick<ValidationResult, 'line' | 'column'> {
const offset = extractOffset(message);
return offset === null ? { line: null, column: null } : offsetToLineCol(text, offset);
}
/**
* Validate a JSON string. Never throws.
* - empty / whitespace-only / non-string → invalid, "Input is empty", no location
* - valid JSON → valid: true
* - invalid JSON → valid: false, cleaned error message,
* and a 1-based line/column when the
* engine reports one
*/
export function validateJson(text: string): ValidationResult {
if (typeof text !== 'string' || text.trim().length === 0) {
return { valid: false, error: 'Input is empty', line: null, column: null };
}
try {
JSON.parse(text);
return { valid: true, error: null, line: null, column: null };
} catch (e) {
const raw = errorMessage(e);
return { valid: false, error: normalizeError(raw), ...locateError(text, raw) };
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →