//! Streaming JSON parser via N-API. //! //! Exposes fast JSON parsing with partial/incomplete JSON recovery //! for use during LLM streaming tool call argument parsing. use napi::bindgen_prelude::*; use napi_derive::napi; /// Parse a complete JSON string. Returns the parsed value or an error. #[napi(js_name = "parseJson")] pub fn parse_json(env: Env, text: String) -> Result { let value: serde_json::Value = serde_json::from_str(&text).map_err(|e| Error::from_reason(format!("{e}")))?; serde_value_to_napi(&env, &value) } /// Parse potentially incomplete JSON by closing unclosed structures. #[napi(js_name = "parsePartialJson")] pub fn parse_partial_json(env: Env, text: String) -> Result { let fixed = fix_partial_json(&text); let value: serde_json::Value = serde_json::from_str(&fixed).map_err(|e| Error::from_reason(format!("{e}")))?; serde_value_to_napi(&env, &value) } /// Try full JSON parse first; fall back to partial parse. Returns `{}` on total failure. #[napi(js_name = "parseStreamingJson")] pub fn parse_streaming_json(env: Env, text: String) -> Result { let trimmed = text.trim(); if trimmed.is_empty() { // Return empty object let obj = env.create_object()?; return Ok(obj.into_unknown()); } // Fast path: try complete parse if let Ok(value) = serde_json::from_str::(trimmed) { return serde_value_to_napi(&env, &value); } // Slow path: fix partial JSON let fixed = fix_partial_json(trimmed); if let Ok(value) = serde_json::from_str::(&fixed) { return serde_value_to_napi(&env, &value); } // Total failure: return empty object let obj = env.create_object()?; Ok(obj.into_unknown()) } /// Fix incomplete JSON by closing unclosed strings, objects, arrays, /// removing trailing commas, and handling truncated values. fn fix_partial_json(input: &str) -> String { let mut result = String::with_capacity(input.len() + 16); let mut stack: Vec = Vec::new(); // tracks expected closing chars let mut in_string = false; let mut escape_next = false; let chars: Vec = input.chars().collect(); let len = chars.len(); let mut i = 0; while i < len { let ch = chars[i]; if escape_next { result.push(ch); escape_next = false; i += 1; continue; } if in_string { if ch == '\\' { result.push(ch); escape_next = true; } else if ch == '"' { result.push(ch); in_string = false; } else { result.push(ch); } i += 1; continue; } // Not in a string match ch { '"' => { result.push(ch); in_string = true; } '{' => { result.push(ch); stack.push('}'); } '[' => { result.push(ch); stack.push(']'); } '}' | ']' => { // Remove trailing comma before closing remove_trailing_comma(&mut result); result.push(ch); if let Some(expected) = stack.last() { if *expected == ch { stack.pop(); } } } _ => { result.push(ch); } } i += 1; } // If we ended inside an escape sequence within a string if escape_next && in_string { // Drop the trailing backslash (incomplete escape) result.pop(); } // Close unclosed string if in_string { result.push('"'); } // Remove any trailing comma before we close structures remove_trailing_comma(&mut result); // Handle truncated values: if last meaningful token looks like a key with colon but no value handle_truncated_value(&mut result); // Close unclosed structures while let Some(closer) = stack.pop() { remove_trailing_comma(&mut result); result.push(closer); } result } /// Remove trailing comma (and whitespace before it) from the result buffer. fn remove_trailing_comma(result: &mut String) { let trimmed_len = result.trim_end().len(); if trimmed_len > 0 { let last_non_ws = result.as_bytes()[trimmed_len - 1]; if last_non_ws == b',' { result.truncate(trimmed_len - 1); } } } /// Handle truncated values after a colon (e.g., `{"key":` or `{"key": tr`) fn handle_truncated_value(result: &mut String) { let trimmed = result.trim_end(); // If ends with colon, add null if trimmed.ends_with(':') { result.push_str("null"); return; } let bytes = trimmed.as_bytes(); let len = bytes.len(); // Check for truncated number: digits (possibly with leading minus, dot, or 'e') // at the end after a value-position character if len > 0 { let last = bytes[len - 1]; if last.is_ascii_digit() || last == b'.' || last == b'-' || last == b'e' || last == b'E' || last == b'+' { // Walk backwards to find the start of the number-like token let mut start = len; while start > 0 { let b = bytes[start - 1]; if b.is_ascii_digit() || b == b'.' || b == b'-' || b == b'e' || b == b'E' || b == b'+' { start -= 1; } else { break; } } if start < len { let before = trimmed[..start].trim_end(); if before.ends_with(':') || before.ends_with(',') || before.ends_with('[') { let token = &trimmed[start..]; // If it doesn't parse as a valid number, truncate to the last valid portion if token.parse::().is_err() { // Strip trailing non-digit chars (e.g. "12." -> "12", "1e" -> "1") let mut valid_end = token.len(); while valid_end > 0 && !token.as_bytes()[valid_end - 1].is_ascii_digit() { valid_end -= 1; } if valid_end > 0 { result.truncate(start + valid_end); } else { // Just a minus or dot with no digits — replace with 0 result.truncate(start); result.push('0'); } } // If it parses fine, leave it as-is return; } } } } // Check for truncated boolean/null literals after a value-position character for prefix in &["tru", "tr", "t", "fals", "fal", "fa", "f", "nul", "nu", "n"] { if trimmed.ends_with(prefix) { let before = trimmed[..len - prefix.len()].trim_end(); if before.ends_with(':') || before.ends_with(',') || before.ends_with('[') { let full = match prefix.as_bytes()[0] { b't' => "true", b'f' => "false", b'n' => "null", _ => unreachable!(), }; result.truncate(len - prefix.len()); result.push_str(full); return; } } } } /// Convert a serde_json::Value to a napi JsUnknown. fn serde_value_to_napi(env: &Env, value: &serde_json::Value) -> Result { match value { serde_json::Value::Null => env.get_null().map(|v| v.into_unknown()), serde_json::Value::Bool(b) => env.get_boolean(*b).map(|v| v.into_unknown()), serde_json::Value::Number(n) => { if let Some(i) = n.as_i64() { // Use i32 if it fits, otherwise f64 if i >= i64::from(i32::MIN) && i <= i64::from(i32::MAX) { env.create_int32(i as i32).map(|v| v.into_unknown()) } else { env.create_double(i as f64).map(|v| v.into_unknown()) } } else if let Some(f) = n.as_f64() { env.create_double(f).map(|v| v.into_unknown()) } else { env.get_null().map(|v| v.into_unknown()) } } serde_json::Value::String(s) => env.create_string(s).map(|v| v.into_unknown()), serde_json::Value::Array(arr) => { let mut js_arr = env.create_array_with_length(arr.len())?; for (idx, item) in arr.iter().enumerate() { let js_val = serde_value_to_napi(env, item)?; js_arr.set_element(idx as u32, js_val)?; } Ok(js_arr.into_unknown()) } serde_json::Value::Object(map) => { let mut obj = env.create_object()?; for (key, val) in map { let js_val = serde_value_to_napi(env, val)?; obj.set_named_property(key, js_val)?; } Ok(obj.into_unknown()) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_fix_complete_json() { let input = r#"{"key": "value", "num": 42}"#; let fixed = fix_partial_json(input); let _: serde_json::Value = serde_json::from_str(&fixed).unwrap(); } #[test] fn test_fix_unclosed_string() { let input = r#"{"key": "val"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v["key"], "val"); } #[test] fn test_fix_unclosed_object() { let input = r#"{"key": "value""#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v["key"], "value"); } #[test] fn test_fix_unclosed_array() { let input = r#"{"arr": [1, 2, 3"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v["arr"].as_array().unwrap().len(), 3); } #[test] fn test_fix_trailing_comma() { let input = r#"{"a": 1, "b": 2,}"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v["a"], 1); assert_eq!(v["b"], 2); } #[test] fn test_fix_truncated_after_colon() { let input = r#"{"key":"#; let fixed = fix_partial_json(input); let _: serde_json::Value = serde_json::from_str(&fixed).unwrap(); } #[test] fn test_fix_truncated_true() { let input = r#"{"key": tr"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v["key"], true); } #[test] fn test_fix_truncated_false() { let input = r#"{"key": fal"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v["key"], false); } #[test] fn test_fix_truncated_null() { let input = r#"{"key": nu"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert!(v["key"].is_null()); } #[test] fn test_fix_nested_partial() { let input = r#"{"a": {"b": [1, 2"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v["a"]["b"].as_array().unwrap().len(), 2); } #[test] fn test_empty_input() { let fixed = fix_partial_json(""); assert_eq!(fixed, ""); } #[test] fn test_fix_trailing_comma_in_array() { let input = r#"[1, 2, 3,]"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v.as_array().unwrap().len(), 3); } #[test] fn test_fix_truncated_number() { let input = r#"{"key": 12"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v["key"], 12); } #[test] fn test_fix_truncated_decimal() { let input = r#"{"key": 3."#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v["key"], 3); } #[test] fn test_fix_truncated_negative_number() { let input = r#"{"key": -"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v["key"], 0); } #[test] fn test_fix_truncated_exponent() { let input = r#"{"key": 1e"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v["key"], 1); } #[test] fn test_fix_truncated_number_in_array() { let input = r#"[1, 42"#; let fixed = fix_partial_json(input); let v: serde_json::Value = serde_json::from_str(&fixed).unwrap(); assert_eq!(v[0], 1); assert_eq!(v[1], 42); } }