/** * JSON Parsing Utilities * * Robust JSON parsing with support for streaming formats and error recovery. * Designed for adapter developers to handle various JSON response formats. * * @module parsers/json-parser */ /** * Options for JSON parsing behavior */ export interface JsonParseOptions { /** Allow newline-delimited JSON (NDJSON) */ allowNDJSON?: boolean; /** Allow multiple top-level JSON values */ allowMultiple?: boolean; /** On parse error, return raw string instead of throwing */ fallbackToString?: boolean; } /** * Parse JSON with support for multiple formats * * Handles: * - Standard JSON objects/arrays * - Newline-delimited JSON (NDJSON) * - Multiple JSON values in sequence * - Malformed JSON (with fallback) * * This function attempts standard JSON parsing first for optimal performance. * If that fails, it tries alternative formats based on the provided options. * * @param text - Raw JSON text to parse * @param options - Parsing options * @returns Parsed JavaScript value(s). For NDJSON or multiple values, returns an array. * @throws Error if parsing fails and fallbackToString is false * * @example * ```typescript * // Standard JSON * parseJSON('{"id": 1}') // Returns: {id: 1} * * // NDJSON * parseJSON('{"id": 1}\n{"id": 2}') // Returns: [{id: 1}, {id: 2}] * * // With options * parseJSON(text, { fallbackToString: true }) // Never throws * * // Disable NDJSON parsing * parseJSON(text, { allowNDJSON: false }) * ``` */ export function parseJSON(text: string, options: JsonParseOptions = {}): unknown { const { allowNDJSON = true, allowMultiple = false, fallbackToString = true } = options; // Try standard JSON.parse first (fastest path) try { return JSON.parse(text); } catch (standardError) { // Standard parse failed, try alternatives // Try NDJSON if (allowNDJSON && text.includes("\n")) { try { const result = parseNDJSON(text); // Only return if we got actual results (not empty array from whitespace-only) if (result.length > 0) { return result; } } catch { // NDJSON parse failed, continue } } // Try multiple values if (allowMultiple) { try { return parseMultipleJSON(text); } catch { // Multiple value parse failed, continue } } // All parsing attempts failed if (fallbackToString) { return text; } throw standardError; } } /** * Parse newline-delimited JSON (NDJSON) * * Each line is treated as a separate JSON object. Empty lines and * whitespace-only lines are automatically filtered out. * * This format is commonly used for: * - Streaming API responses * - Log files * - Bulk data exports * - Server-sent events * * @param text - NDJSON text * @returns Array of parsed JSON objects * @throws Error if any line contains invalid JSON * * @example * ```typescript * parseNDJSON('{"id": 1}\n{"id": 2}') * // Returns: [{id: 1}, {id: 2}] * * // Handles empty lines * parseNDJSON('{"id": 1}\n\n{"id": 2}') * // Returns: [{id: 1}, {id: 2}] * * // Handles whitespace * parseNDJSON(' {"id": 1} \n {"id": 2} ') * // Returns: [{id: 1}, {id: 2}] * ``` */ export function parseNDJSON(text: string): unknown[] { const lines = text.split("\n").filter((line) => line.trim()); return lines.map((line) => JSON.parse(line)); } /** * Parse multiple JSON values in sequence * * Handles concatenated JSON objects/arrays without delimiters. * Uses a state machine to track braces/brackets and find complete JSON values. * * Example: {"a":1}{"b":2} -> [{"a":1}, {"b":2}] * * **Limitations**: * - Only supports objects and arrays as JSON values * - Does not support primitive values (numbers, strings, booleans, null) * - Example: `42true"hello"` will fail to parse correctly * * **Note**: This limitation is acceptable because: * - `allowMultiple` defaults to `false` in `parseJSON()` * - Concatenated primitive JSON values are extremely rare in practice * - Standard NDJSON (newline-delimited) is the recommended format for multiple values * * @param text - Multiple JSON values concatenated * @returns Array of parsed JSON values * @throws Error if text is not valid concatenated JSON * * @example * ```typescript * // Objects and arrays work * parseMultipleJSON('{"a":1}{"b":2}') * // Returns: [{a: 1}, {b: 2}] * * parseMultipleJSON('[1,2][3,4]') * // Returns: [[1, 2], [3, 4]] * * // Handles whitespace between values * parseMultipleJSON('{"a":1} {"b":2}') * // Returns: [{a: 1}, {b: 2}] * * // Primitives are NOT supported (use NDJSON instead) * // parseMultipleJSON('42true"hello"') // ❌ Will fail * * // For multiple primitive values, use NDJSON: * parseNDJSON('42\ntrue\n"hello"') // ✅ Works correctly * // Returns: [42, true, "hello"] * ``` */ function parseMultipleJSON(text: string): unknown[] { const results: unknown[] = []; let current = 0; const trimmed = text.trim(); while (current < trimmed.length) { // Find next complete JSON value let depth = 0; let inString = false; let escape = false; let start = current; // Skip whitespace while (current < trimmed.length && /\s/.test(trimmed.charAt(current))) { current++; start = current; } if (current >= trimmed.length) break; // Scan for complete JSON value while (current < trimmed.length) { const char = trimmed.charAt(current); if (escape) { escape = false; current++; continue; } if (char === "\\") { escape = true; current++; continue; } if (char === '"') { inString = !inString; current++; continue; } if (inString) { current++; continue; } if (char === "{" || char === "[") { depth++; } else if (char === "}" || char === "]") { depth--; if (depth === 0) { // Complete value found results.push(JSON.parse(trimmed.substring(start, current + 1))); current++; break; } } current++; } if (depth !== 0) { const preview = trimmed.substring(start, Math.min(start + 50, trimmed.length)); throw new Error( `Incomplete JSON value at position ${start}. ` + `Preview: "${preview}${trimmed.length > start + 50 ? "..." : ""}"` ); } } if (results.length === 0) { const preview = trimmed.substring(0, Math.min(50, trimmed.length)); throw new Error( `No valid JSON values found in input. ` + `Preview: "${preview}${trimmed.length > 50 ? "..." : ""}"` ); } return results; }