Skip to content

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 →