Skip to main content

oxicode_ai/utils/
json_parse.rs

1//! Robust JSON parsing utilities
2//!
3//! Handles malformed JSON from streaming LLM responses:
4//! - Escapes raw control characters inside strings
5//! - Doubles backslashes before invalid escape characters
6//! - Repairs incomplete JSON from streaming responses
7
8use crate::messages::AssistantMessage;
9
10/// Characters that are valid after a backslash in JSON strings
11const VALID_JSON_ESCAPES: &[char] = &['"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u'];
12
13/// Check if a character is a control character (U+0000 to U+001F)
14fn is_control_character(ch: char) -> bool {
15    ch as u32 <= 0x1F
16}
17
18/// Escape a control character for JSON
19fn escape_control_character(ch: char) -> String {
20    match ch {
21        '\u{0008}' => "\\b".to_string(),
22        '\u{000C}' => "\\f".to_string(),
23        '\n' => "\\n".to_string(),
24        '\r' => "\\r".to_string(),
25        '\t' => "\\t".to_string(),
26        _ => format!("\\u{:04x}", ch as u32),
27    }
28}
29
30/// Repairs malformed JSON string literals by:
31/// - Escaping raw control characters inside strings
32/// - Doubling backslashes before invalid escape characters
33pub fn repair_json(json: &str) -> String {
34    let mut repaired = String::with_capacity(json.len());
35    let mut in_string = false;
36    let chars: Vec<char> = json.chars().collect();
37    let len = chars.len();
38    let mut index = 0;
39
40    while index < len {
41        let ch = chars[index];
42
43        if !in_string {
44            repaired.push(ch);
45            if ch == '"' {
46                in_string = true;
47            }
48            index += 1;
49            continue;
50        }
51
52        // We're inside a string
53        if ch == '"' {
54            repaired.push(ch);
55            in_string = false;
56            index += 1;
57            continue;
58        }
59
60        if ch == '\\' {
61            // Check next character
62            if index + 1 >= len {
63                // Trailing backslash at end - escape it
64                repaired.push_str("\\\\");
65                index += 1;
66                continue;
67            }
68
69            let next_ch = chars[index + 1];
70
71            if next_ch == 'u' {
72                // Unicode escape - check if valid
73                let unicode_digits: String = chars[index + 2..std::cmp::min(index + 6, len)]
74                    .iter()
75                    .collect();
76                if unicode_digits.len() == 4
77                    && unicode_digits.chars().all(|c| c.is_ascii_hexdigit())
78                {
79                    repaired.push_str(&format!("\\u{}", unicode_digits));
80                    index += 6;
81                    continue;
82                }
83            }
84
85            if VALID_JSON_ESCAPES.contains(&next_ch) {
86                repaired.push('\\');
87                repaired.push(next_ch);
88                index += 2;
89                continue;
90            }
91
92            // Invalid escape - double the backslash
93            repaired.push_str("\\\\");
94            index += 1;
95            continue;
96        }
97
98        // Regular character in string - escape control characters
99        if is_control_character(ch) {
100            repaired.push_str(&escape_control_character(ch));
101        } else {
102            repaired.push(ch);
103        }
104        index += 1;
105    }
106
107    repaired
108}
109
110/// Parse JSON with automatic repair of common malformations.
111///
112/// First tries standard parsing. If that fails, repairs the JSON and retries.
113pub fn parse_json_with_repair<T: serde::de::DeserializeOwned>(
114    json: &str,
115) -> Result<T, serde_json::Error> {
116    match serde_json::from_str(json) {
117        Ok(result) => Ok(result),
118        Err(original_error) => {
119            let repaired = repair_json(json);
120            if repaired != json {
121                match serde_json::from_str(&repaired) {
122                    Ok(result) => Ok(result),
123                    Err(_) => Err(original_error),
124                }
125            } else {
126                Err(original_error)
127            }
128        }
129    }
130}
131
132/// Attempts to parse potentially incomplete JSON from a streaming response.
133///
134/// Tries multiple strategies:
135/// 1. Direct parse
136/// 2. Parse with repair
137/// 3. Truncate at last valid position and retry
138///
139/// Always returns a valid value, using `default` as fallback.
140pub fn parse_streaming_json<T: serde::de::DeserializeOwned + Default>(json: &str) -> T {
141    let trimmed = json.trim();
142    if trimmed.is_empty() {
143        return T::default();
144    }
145
146    // Strategy 1: Direct parse
147    if let Ok(result) = serde_json::from_str(trimmed) {
148        return result;
149    }
150
151    // Strategy 2: Parse with repair
152    if let Ok(result) = parse_json_with_repair(trimmed) {
153        return result;
154    }
155
156    // Strategy 3: Try to parse as partial by finding last complete object
157    if let Some(result) = parse_partial_json(trimmed) {
158        return result;
159    }
160
161    // Strategy 4: Repair then parse partial
162    let repaired = repair_json(trimmed);
163    if repaired != trimmed
164        && let Some(result) = parse_partial_json(&repaired)
165    {
166        return result;
167    }
168
169    T::default()
170}
171
172/// Try to parse partial JSON by progressively truncating from the end
173/// until we find valid JSON.
174fn parse_partial_json<T: serde::de::DeserializeOwned>(json: &str) -> Option<T> {
175    // Only try this for objects/arrays
176    let trimmed = json.trim();
177    if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
178        return None;
179    }
180
181    let _close_char = if trimmed.starts_with('{') { '}' } else { ']' };
182    let _open_char = if trimmed.starts_with('{') { '{' } else { '[' };
183
184    // Track nesting depth
185    let mut depth = 0;
186    let mut in_string = false;
187    let mut last_valid_close = None;
188    let bytes = trimmed.as_bytes();
189
190    for (i, &b) in bytes.iter().enumerate() {
191        if in_string {
192            if b == b'"' {
193                in_string = false;
194            } else if b == b'\\' {
195                // Skip next char (escape)
196                continue;
197            }
198            continue;
199        }
200
201        match b {
202            b'"' => in_string = true,
203            b'{' | b'[' => depth += 1,
204            b'}' | b']' => {
205                depth -= 1;
206                if depth == 0 {
207                    last_valid_close = Some(i);
208                }
209            }
210            _ => {}
211        }
212    }
213
214    // If we found a valid closing position, try to parse up to it
215    if let Some(pos) = last_valid_close {
216        let candidate = &trimmed[..=pos];
217        if let Ok(result) = serde_json::from_str(candidate) {
218            return Some(result);
219        }
220    }
221
222    None
223}
224
225/// Parse a streaming SSE data field as JSON, with robust error handling.
226/// Returns `None` for non-data lines or unparseable content.
227pub fn parse_sse_data<T: serde::de::DeserializeOwned + Default>(line: &str) -> Option<T> {
228    let line = line.trim();
229
230    if !line.starts_with("data: ") {
231        return None;
232    }
233
234    let data = &line[6..];
235
236    if data.is_empty() || data == "[DONE]" {
237        return None;
238    }
239
240    Some(parse_streaming_json(data))
241}
242
243/// Extract the error message from an assistant message that may have
244/// malformed JSON in its error field.
245pub fn extract_error_message(message: &AssistantMessage) -> String {
246    message
247        .error_message
248        .clone()
249        .unwrap_or_else(|| "Unknown error".to_string())
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use serde::Deserialize;
256
257    #[derive(Debug, Deserialize, PartialEq, Default)]
258    struct TestObj {
259        name: String,
260        value: Option<i64>,
261    }
262
263    #[test]
264    fn test_repair_json_valid() {
265        let json = r#"{"name": "test"}"#;
266        assert_eq!(repair_json(json), json);
267    }
268
269    #[test]
270    fn test_repair_json_control_chars() {
271        let json = "{\"name\": \"hello\nworld\"}";
272        let repaired = repair_json(json);
273        assert!(repaired.contains("\\n"));
274        assert!(!repaired.contains("hello\nworld"));
275    }
276
277    #[test]
278    fn test_repair_json_tab() {
279        let json = "{\"name\": \"hello\tworld\"}";
280        let repaired = repair_json(json);
281        assert!(repaired.contains("\\t"));
282    }
283
284    #[test]
285    fn test_repair_json_invalid_escape() {
286        let json = r#"{"name": "hello\qworld"}"#;
287        let repaired = repair_json(json);
288        assert!(repaired.contains("\\\\q") || repaired.contains(r#"\\q"#));
289    }
290
291    #[test]
292    fn test_repair_json_trailing_backslash() {
293        let json = r#"{"name": "test\"#;
294        let repaired = repair_json(json);
295        assert!(repaired.contains("\\\\"));
296    }
297
298    #[test]
299    fn test_repair_json_valid_escapes_preserved() {
300        let json = r#"{"name": "hello\nworld"}"#;
301        let repaired = repair_json(json);
302        assert_eq!(repaired, json);
303    }
304
305    #[test]
306    fn test_repair_json_unicode_escape_preserved() {
307        let json = r#"{"name": "\u0041"}"#;
308        let repaired = repair_json(json);
309        assert_eq!(repaired, json);
310    }
311
312    #[test]
313    fn test_parse_json_with_repair_valid() {
314        let result: TestObj = parse_json_with_repair(r#"{"name": "test", "value": 42}"#).unwrap();
315        assert_eq!(result.name, "test");
316        assert_eq!(result.value, Some(42));
317    }
318
319    #[test]
320    fn test_parse_json_with_repair_control_chars() {
321        let json = "{\"name\": \"hello\nworld\"}";
322        let result: TestObj = parse_json_with_repair(json).unwrap();
323        assert_eq!(result.name, "hello\nworld");
324    }
325
326    #[test]
327    fn test_parse_streaming_json_valid() {
328        let result: TestObj = parse_streaming_json(r#"{"name": "test"}"#);
329        assert_eq!(result.name, "test");
330    }
331
332    #[test]
333    fn test_parse_streaming_json_empty() {
334        let result: TestObj = parse_streaming_json("");
335        assert_eq!(result, TestObj::default());
336    }
337
338    #[test]
339    fn test_parse_streaming_json_whitespace() {
340        let result: TestObj = parse_streaming_json("   ");
341        assert_eq!(result, TestObj::default());
342    }
343
344    #[test]
345    fn test_parse_streaming_json_partial() {
346        let result: TestObj = parse_streaming_json(r#"{"name": "test"}, "extra""#);
347        assert_eq!(result.name, "test");
348    }
349
350    #[test]
351    fn test_parse_sse_data_valid() {
352        let result: TestObj = parse_sse_data(r#"data: {"name": "test"}"#).unwrap();
353        assert_eq!(result.name, "test");
354    }
355
356    #[test]
357    fn test_parse_sse_data_done() {
358        let result: Option<TestObj> = parse_sse_data("data: [DONE]");
359        assert!(result.is_none());
360    }
361
362    #[test]
363    fn test_parse_sse_data_not_data_line() {
364        let result: Option<TestObj> = parse_sse_data("event: message");
365        assert!(result.is_none());
366    }
367
368    #[test]
369    fn test_parse_sse_data_empty_data() {
370        let result: Option<TestObj> = parse_sse_data("data: ");
371        assert!(result.is_none());
372    }
373
374    #[test]
375    fn test_escape_control_character_special() {
376        assert_eq!(escape_control_character('\n'), "\\n");
377        assert_eq!(escape_control_character('\r'), "\\r");
378        assert_eq!(escape_control_character('\t'), "\\t");
379        assert_eq!(escape_control_character('\u{0008}'), "\\b");
380        assert_eq!(escape_control_character('\u{000C}'), "\\f");
381    }
382
383    #[test]
384    fn test_escape_control_character_generic() {
385        assert_eq!(escape_control_character('\u{0001}'), "\\u0001");
386        assert_eq!(escape_control_character('\u{001F}'), "\\u001f");
387    }
388}