1use serde::de::DeserializeOwned;
12
13#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum JsonRepairError {
17 #[error("JSON repair failed: {0}")]
19 RepairFailed(String),
20
21 #[error("Deserialization failed: {details}")]
23 DeserializationFailed {
24 details: String,
26 },
27}
28
29pub fn parse_tolerant_json<T: DeserializeOwned>(raw: &str) -> Result<T, JsonRepairError> {
40 let repaired = repair_json(raw)?;
41 serde_json::from_str::<T>(&repaired).map_err(|e| JsonRepairError::DeserializationFailed {
42 details: format!("{} (repaired JSON: {})", e, truncate(&repaired, 200)),
43 })
44}
45
46pub fn repair_json(raw: &str) -> Result<String, JsonRepairError> {
50 let trimmed = raw.trim();
51
52 let stripped = strip_code_fences(trimmed);
54
55 let extracted = extract_bracket_pair(&stripped);
57
58 let quotes_fixed = fix_unescaped_quotes(&extracted);
60
61 let no_trailing = remove_trailing_commas("es_fixed);
63
64 let truncated = truncate_to_matching_bracket(&no_trailing);
66
67 if truncated.is_empty() {
69 return Err(JsonRepairError::RepairFailed(
70 "no JSON content found".to_string(),
71 ));
72 }
73
74 Ok(truncated)
75}
76
77pub fn strip_code_fences(text: &str) -> String {
79 let trimmed = text.trim();
80 if let Some(rest) = trimmed.strip_prefix("```json") {
81 if let Some(end) = rest.find("```") {
82 return rest[..end].trim().to_string();
83 }
84 return rest.trim().to_string();
86 }
87 if let Some(rest) = trimmed.strip_prefix("```") {
88 if let Some(end) = rest.find("```") {
89 return rest[..end].trim().to_string();
90 }
91 return rest.trim().to_string();
92 }
93 trimmed.to_string()
94}
95
96pub fn extract_bracket_pair(text: &str) -> String {
98 let bytes = text.as_bytes();
99 let start_idx = text.find(['[', '{']);
100
101 if let Some(start) = start_idx {
102 let open = bytes[start];
103 let close = if open == b'[' { b']' } else { b'}' };
104
105 let mut depth = 0i32;
106 let mut in_string = false;
107 let mut escape_next = false;
108
109 for i in start..bytes.len() {
110 let ch = bytes[i];
111 if escape_next {
112 escape_next = false;
113 continue;
114 }
115 if ch == b'\\' && in_string {
116 escape_next = true;
117 continue;
118 }
119 if ch == b'"' {
120 in_string = !in_string;
121 continue;
122 }
123 if in_string {
124 continue;
125 }
126 if ch == open {
127 depth += 1;
128 } else if ch == close {
129 depth -= 1;
130 if depth == 0 {
131 return text[start..=i].to_string();
132 }
133 }
134 }
135 }
136
137 text.to_string()
138}
139
140pub fn fix_unescaped_quotes(json: &str) -> String {
150 let chars: Vec<char> = json.chars().collect();
151 let n = chars.len();
152 let mut result = String::with_capacity(json.len());
153 let mut in_string = false;
154 let mut escape_next = false;
155
156 for i in 0..n {
157 let c = chars[i];
158
159 if in_string {
160 if escape_next {
161 result.push(c);
162 escape_next = false;
163 continue;
164 }
165 if c == '\\' {
166 result.push(c);
167 escape_next = true;
168 continue;
169 }
170 if c == '"' {
171 let mut j = i + 1;
173 while j < n && chars[j].is_whitespace() {
174 j += 1;
175 }
176 let is_closing = j >= n || matches!(chars[j], ':' | ',' | ']' | '}');
177 if is_closing {
178 in_string = false;
179 result.push(c);
180 } else {
181 result.push('\\');
183 result.push(c);
184 }
185 continue;
186 }
187 result.push(c);
188 } else if c == '"' {
189 in_string = true;
190 result.push(c);
191 } else {
192 result.push(c);
193 }
194 }
195
196 result
197}
198
199pub fn remove_trailing_commas(json: &str) -> String {
209 let mut result = String::with_capacity(json.len());
210 let chars: Vec<char> = json.chars().collect();
211 let len = chars.len();
212 let mut in_string = false;
213 let mut escape_next = false;
214
215 for i in 0..len {
216 let ch = chars[i];
217 if in_string {
218 result.push(ch);
219 if escape_next {
220 escape_next = false;
221 } else if ch == '\\' {
222 escape_next = true;
223 } else if ch == '"' {
224 in_string = false;
225 }
226 continue;
227 }
228 if ch == '"' {
229 in_string = true;
230 result.push(ch);
231 continue;
232 }
233 if ch == ',' {
235 let mut j = i + 1;
236 while j < len && chars[j].is_whitespace() {
237 j += 1;
238 }
239 if j < len && (chars[j] == ']' || chars[j] == '}') {
240 continue;
242 }
243 }
244 result.push(ch);
245 }
246
247 result
248}
249
250pub fn truncate_to_matching_bracket(json: &str) -> String {
252 let trimmed = json.trim_end();
254 if trimmed.ends_with(']') || trimmed.ends_with('}') {
255 return trimmed.to_string();
256 }
257
258 if let Some(last_close) = trimmed.rfind([']', '}']) {
260 return trimmed[..=last_close].to_string();
261 }
262
263 json.to_string()
264}
265
266fn truncate(s: &str, max_len: usize) -> String {
268 if s.len() <= max_len {
269 s.to_string()
270 } else {
271 let end = s
272 .char_indices()
273 .take(max_len)
274 .last()
275 .map(|(i, _)| i)
276 .unwrap_or(0);
277 format!("{}...", &s[..end])
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use serde::Deserialize;
285
286 #[derive(Debug, Deserialize, PartialEq)]
287 struct TestStruct {
288 name: String,
289 value: i32,
290 }
291
292 #[test]
293 fn test_parse_valid_json() {
294 let raw = r#"{"name": "test", "value": 42}"#;
295 let result: TestStruct = parse_tolerant_json(raw).unwrap();
296 assert_eq!(result.name, "test");
297 assert_eq!(result.value, 42);
298 }
299
300 #[test]
301 fn test_parse_json_with_code_fence() {
302 let raw = "```json\n{\"name\": \"test\", \"value\": 42}\n```";
303 let result: TestStruct = parse_tolerant_json(raw).unwrap();
304 assert_eq!(result.name, "test");
305 }
306
307 #[test]
308 fn test_parse_json_with_trailing_comma() {
309 let raw = r#"{"name": "test", "value": 42,}"#;
310 let result: TestStruct = parse_tolerant_json(raw).unwrap();
311 assert_eq!(result.name, "test");
312 assert_eq!(result.value, 42);
313 }
314
315 #[test]
316 fn test_parse_json_with_surrounding_text() {
317 let raw = "Here is the result: {\"name\": \"test\", \"value\": 42} done.";
318 let result: TestStruct = parse_tolerant_json(raw).unwrap();
319 assert_eq!(result.name, "test");
320 }
321
322 #[test]
323 fn test_parse_json_with_trailing_garbage() {
324 let raw = r#"{"name": "test", "value": 42} and some extra text"#;
325 let result: TestStruct = parse_tolerant_json(raw).unwrap();
326 assert_eq!(result.name, "test");
327 }
328
329 #[test]
330 fn test_parse_json_with_unescaped_inner_quotes() {
331 let raw = r#"{"name": "He said "hi" and left", "value": 1}"#;
333 let result: TestStruct = parse_tolerant_json(raw).unwrap();
334 assert_eq!(result.name, "He said \"hi\" and left");
335 assert_eq!(result.value, 1);
336 }
337
338 #[test]
339 fn test_parse_json_array_with_trailing_comma() {
340 let raw = r#"[{"name": "a", "value": 1}, {"name": "b", "value": 2},]"#;
341 let result: Vec<TestStruct> = parse_tolerant_json(raw).unwrap();
342 assert_eq!(result.len(), 2);
343 }
344
345 #[test]
346 fn test_parse_empty_text_fails() {
347 let result: Result<TestStruct, _> = parse_tolerant_json("");
348 assert!(result.is_err());
349 }
350
351 #[test]
352 fn test_parse_no_json_content_fails() {
353 let result: Result<TestStruct, _> = parse_tolerant_json("just some plain text");
354 assert!(result.is_err());
355 }
356
357 #[test]
358 fn test_fix_unescaped_quotes_inner_only() {
359 let input = r#"{"a": "He said "hi"", "b": 2}"#;
360 assert_eq!(
361 fix_unescaped_quotes(input),
362 r#"{"a": "He said \"hi\"", "b": 2}"#
363 );
364 }
365
366 #[test]
367 fn test_fix_unescaped_quotes_leaves_closing_quotes() {
368 let input = r#"{"key": "value", "n": 1}"#;
369 assert_eq!(fix_unescaped_quotes(input), input);
370 }
371
372 #[test]
373 fn test_fix_unescaped_quotes_empty_string() {
374 let input = r#"{"a": ""}"#;
375 assert_eq!(fix_unescaped_quotes(input), input);
376 }
377
378 #[test]
379 fn test_strip_code_fences_json() {
380 let input = "```json\n{\"key\": \"val\"}\n```";
381 assert_eq!(strip_code_fences(input), "{\"key\": \"val\"}");
382 }
383
384 #[test]
385 fn test_strip_code_fences_plain() {
386 let input = "```\n{\"key\": \"val\"}\n```";
387 assert_eq!(strip_code_fences(input), "{\"key\": \"val\"}");
388 }
389
390 #[test]
391 fn test_strip_code_fences_no_fence() {
392 let input = "{\"key\": \"val\"}";
393 assert_eq!(strip_code_fences(input), "{\"key\": \"val\"}");
394 }
395
396 #[test]
397 fn test_remove_trailing_commas_object() {
398 let input = r#"{"a": 1, "b": 2,}"#;
399 assert_eq!(remove_trailing_commas(input), r#"{"a": 1, "b": 2}"#);
400 }
401
402 #[test]
403 fn test_remove_trailing_commas_array() {
404 let input = r#"[1, 2, 3,]"#;
405 assert_eq!(remove_trailing_commas(input), r#"[1, 2, 3]"#);
406 }
407
408 #[test]
409 fn test_remove_trailing_commas_nested() {
410 let input = r#"{"arr": [1, 2,], "val": 3,}"#;
411 assert_eq!(
412 remove_trailing_commas(input),
413 r#"{"arr": [1, 2], "val": 3}"#
414 );
415 }
416
417 #[test]
418 fn test_remove_trailing_commas_preserves_string_literals() {
419 let input = r#"{"a": "text, } more"}"#;
421 assert_eq!(remove_trailing_commas(input), input);
422
423 let input = r#"{"a": "x,]", "b": 1}"#;
424 assert_eq!(remove_trailing_commas(input), input);
425
426 let input = r#"{"a": "he said \"hi, \"", "b": 1,}"#;
427 assert_eq!(
428 remove_trailing_commas(input),
429 r#"{"a": "he said \"hi, \"", "b": 1}"#
430 );
431
432 let input = r#"{"a": "1, 2, 3", "b": [1,]}"#;
433 assert_eq!(
434 remove_trailing_commas(input),
435 r#"{"a": "1, 2, 3", "b": [1]}"#
436 );
437 }
438
439 #[test]
440 fn test_extract_bracket_pair_array() {
441 let input = "prefix [1, 2, 3] suffix";
442 assert_eq!(extract_bracket_pair(input), "[1, 2, 3]");
443 }
444
445 #[test]
446 fn test_extract_bracket_pair_object() {
447 let input = r#"text {"a": 1} more"#;
448 assert_eq!(extract_bracket_pair(input), r#"{"a": 1}"#);
449 }
450
451 #[test]
452 fn test_parse_json_no_closing_fence() {
453 let raw = "```json\n{\"name\": \"test\", \"value\": 42}";
454 let result: TestStruct = parse_tolerant_json(raw).unwrap();
455 assert_eq!(result.name, "test");
456 }
457
458 #[test]
459 fn test_error_display() {
460 let err = JsonRepairError::RepairFailed("no json".to_string());
461 assert!(err.to_string().contains("no json"));
462 }
463}