parseJsonSafely<T>() with line/column errors
TypeScript
import { readFile } from 'node:fs/promises';
/** Error carrying the character offset reported by JSON.parse. */
export class JsonParseError extends Error {
readonly position: number | undefined;
constructor(message: string, position?: number) {
super(message);
this.name = 'JsonParseError';
this.position = position;
}
}
/** Parse JSON text, turning SyntaxError into a message with line and column. */
export function parseJsonSafely<T = unknown>(text: string, source = '<input>'): T {
try {
return JSON.parse(text) as T;
} catch (error) {
if (!(error instanceof SyntaxError)) throw error;
const match = /position\s+(\d+)/i.exec(error.message);
const position = match ? Number(match[1]) : undefined;
if (position === undefined || Number.isNaN(position)) {
throw new JsonParseError(`Invalid JSON from ${source}: ${error.message}`);
}
const before = text.slice(0, position);
const line = before.split('\n').length;
const column = position - before.lastIndexOf('\n');
const near = text.slice(Math.max(0, position - 20), position + 20).replace(/\s+/g, ' ');
throw new JsonParseError(`Invalid JSON at ${source}:${line}:${column} near '${near}'`, position);
}
}
/** Read a JSON file from disk and parse it with the same diagnostics. */
export async function parseJsonFile<T>(path: string): Promise<T> {
return parseJsonSafely<T>(await readFile(path, 'utf8'), path);
}