pub fn repair_json(input: &str) -> String {
let stripped = strip_markdown_fences(input);
let no_trailing = fix_trailing_commas(&stripped);
let quoted_keys = fix_unquoted_keys(&no_trailing);
close_truncated("ed_keys)
}
pub fn parse_with_repair(input: &str) -> Result<serde_json::Value, serde_json::Error> {
if let Ok(val) = serde_json::from_str(input) {
return Ok(val);
}
let repaired = repair_json(input);
serde_json::from_str(&repaired)
}
fn strip_markdown_fences(input: &str) -> String {
let trimmed = input.trim();
let after_open = if let Some(stripped) = trimmed.strip_prefix("```json") {
stripped
} else if let Some(stripped) = trimmed.strip_prefix("```") {
stripped
} else {
return trimmed.to_string();
};
let after_open = after_open.strip_prefix('\n').unwrap_or(after_open);
let before_close = if let Some(stripped) = after_open.trim_end().strip_suffix("```") {
stripped.trim_end()
} else {
after_open
};
before_close.to_string()
}
fn fix_trailing_commas(input: &str) -> String {
let chars: Vec<char> = input.chars().collect();
let len = chars.len();
let mut result = String::with_capacity(len);
let mut in_string = false;
let mut escape_next = false;
let mut i = 0;
while i < len {
let c = chars[i];
if escape_next {
result.push(c);
escape_next = false;
i += 1;
continue;
}
if c == '\\' && in_string {
result.push(c);
escape_next = true;
i += 1;
continue;
}
if c == '"' {
in_string = !in_string;
result.push(c);
i += 1;
continue;
}
if in_string {
result.push(c);
i += 1;
continue;
}
if c == ',' {
let mut j = i + 1;
while j < len && chars[j].is_whitespace() {
j += 1;
}
if j < len && (chars[j] == ']' || chars[j] == '}') {
i += 1;
continue;
}
}
result.push(c);
i += 1;
}
result
}
fn fix_unquoted_keys(input: &str) -> String {
let chars: Vec<char> = input.chars().collect();
let len = chars.len();
let mut result = String::with_capacity(len + 32);
let mut in_string = false;
let mut escape_next = false;
let mut i = 0;
while i < len {
let c = chars[i];
if escape_next {
result.push(c);
escape_next = false;
i += 1;
continue;
}
if c == '\\' && in_string {
result.push(c);
escape_next = true;
i += 1;
continue;
}
if c == '"' {
in_string = !in_string;
result.push(c);
i += 1;
continue;
}
if in_string {
result.push(c);
i += 1;
continue;
}
if is_key_start_char(c) {
let start = i;
while i < len && is_key_char(chars[i]) {
i += 1;
}
let key = &chars[start..i];
let mut j = i;
while j < len && chars[j].is_whitespace() {
j += 1;
}
if j < len && chars[j] == ':' {
result.push('"');
result.extend(key);
result.push('"');
} else {
result.extend(key);
}
continue;
}
result.push(c);
i += 1;
}
result
}
fn is_key_start_char(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_' || c == '$'
}
fn is_key_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '$'
}
fn close_truncated(input: &str) -> String {
let mut stack: Vec<char> = Vec::new();
let mut in_string = false;
let mut escape_next = false;
for c in input.chars() {
if escape_next {
escape_next = false;
continue;
}
if c == '\\' && in_string {
escape_next = true;
continue;
}
if c == '"' {
in_string = !in_string;
continue;
}
if in_string {
continue;
}
match c {
'{' => stack.push('}'),
'[' => stack.push(']'),
'}' | ']' => {
if stack.last() == Some(&c) {
stack.pop();
}
}
_ => {}
}
}
if !in_string && stack.is_empty() {
return input.to_string();
}
let mut result = input.to_string();
if in_string {
result.push('"');
}
for closer in stack.into_iter().rev() {
result.push(closer);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_json_fence() {
let input = "```json\n{\"key\": \"value\"}\n```";
assert_eq!(strip_markdown_fences(input), "{\"key\": \"value\"}");
}
#[test]
fn strip_plain_fence() {
let input = "```\n{\"a\": 1}\n```";
assert_eq!(strip_markdown_fences(input), "{\"a\": 1}");
}
#[test]
fn strip_fence_with_whitespace() {
let input = " ```json\n{\"x\": true}\n``` ";
assert_eq!(strip_markdown_fences(input), "{\"x\": true}");
}
#[test]
fn no_fence_unchanged() {
let input = "{\"a\": 1}";
assert_eq!(strip_markdown_fences(input), input);
}
#[test]
fn fence_without_closing() {
let input = "```json\n{\"a\": 1}";
assert_eq!(strip_markdown_fences(input), "{\"a\": 1}");
}
#[test]
fn trailing_comma_object() {
let input = r#"{"a": 1, "b": 2,}"#;
let fixed = fix_trailing_commas(input);
assert_eq!(fixed, r#"{"a": 1, "b": 2}"#);
}
#[test]
fn trailing_comma_array() {
let input = "[1, 2, 3,]";
let fixed = fix_trailing_commas(input);
assert_eq!(fixed, "[1, 2, 3]");
}
#[test]
fn trailing_comma_nested() {
let input = r#"{"a": [1, 2,], "b": {"c": 3,},}"#;
let fixed = fix_trailing_commas(input);
assert_eq!(fixed, r#"{"a": [1, 2], "b": {"c": 3}}"#);
}
#[test]
fn trailing_comma_with_whitespace() {
let input = "{\n \"a\": 1,\n}";
let fixed = fix_trailing_commas(input);
assert_eq!(fixed, "{\n \"a\": 1\n}");
}
#[test]
fn no_trailing_comma_unchanged() {
let input = r#"{"a": 1, "b": 2}"#;
let fixed = fix_trailing_commas(input);
assert_eq!(fixed, input);
}
#[test]
fn comma_in_string_preserved() {
let input = r#"{"a": "hello,}"#;
let fixed = fix_trailing_commas(input);
assert_eq!(fixed, r#"{"a": "hello,}"#);
}
#[test]
fn unquoted_simple_key() {
let input = r#"{name: "test"}"#;
let fixed = fix_unquoted_keys(input);
assert_eq!(fixed, r#"{"name": "test"}"#);
}
#[test]
fn unquoted_multiple_keys() {
let input = r#"{name: "test", value: 42}"#;
let fixed = fix_unquoted_keys(input);
assert_eq!(fixed, r#"{"name": "test", "value": 42}"#);
}
#[test]
fn unquoted_nested_keys() {
let input = r#"{outer: {inner: "val"}}"#;
let fixed = fix_unquoted_keys(input);
assert_eq!(fixed, r#"{"outer": {"inner": "val"}}"#);
}
#[test]
fn quoted_keys_unchanged() {
let input = r#"{"name": "test", "value": 42}"#;
let fixed = fix_unquoted_keys(input);
assert_eq!(fixed, input);
}
#[test]
fn boolean_values_not_quoted() {
let input = r#"{"a": true, "b": false, "c": null}"#;
let fixed = fix_unquoted_keys(input);
assert_eq!(fixed, input);
}
#[test]
fn unquoted_key_with_underscore() {
let input = r#"{my_key: "val"}"#;
let fixed = fix_unquoted_keys(input);
assert_eq!(fixed, r#"{"my_key": "val"}"#);
}
#[test]
fn missing_close_brace() {
let input = r#"{"a": 1"#;
let fixed = close_truncated(input);
assert_eq!(fixed, r#"{"a": 1}"#);
}
#[test]
fn missing_close_bracket() {
let input = "[1, 2, 3";
let fixed = close_truncated(input);
assert_eq!(fixed, "[1, 2, 3]");
}
#[test]
fn missing_nested_closers() {
let input = r#"{"a": [1, 2, {"b": 3"#;
let fixed = close_truncated(input);
assert_eq!(fixed, r#"{"a": [1, 2, {"b": 3}]}"#);
}
#[test]
fn balanced_unchanged() {
let input = r#"{"a": [1, 2]}"#;
let fixed = close_truncated(input);
assert_eq!(fixed, input);
}
#[test]
fn truncated_in_string() {
let input = r#"{"a": "hello {world"#;
let fixed = close_truncated(input);
assert_eq!(fixed, r#"{"a": "hello {world"}"#);
}
#[test]
fn repair_fenced_with_trailing_comma() {
let input = "```json\n{\"key\": \"value\",}\n```";
let repaired = repair_json(input);
let val: serde_json::Value = serde_json::from_str(&repaired).unwrap();
assert_eq!(val["key"], "value");
}
#[test]
fn repair_truncated_with_unquoted_keys() {
let input = r#"{name: "test", count: 42"#;
let repaired = repair_json(input);
let val: serde_json::Value = serde_json::from_str(&repaired).unwrap();
assert_eq!(val["name"], "test");
assert_eq!(val["count"], 42);
}
#[test]
fn repair_all_issues_combined() {
let input = "```json\n{name: \"test\", items: [1, 2,],}\n```";
let repaired = repair_json(input);
let val: serde_json::Value = serde_json::from_str(&repaired).unwrap();
assert_eq!(val["name"], "test");
assert_eq!(val["items"], serde_json::json!([1, 2]));
}
#[test]
fn repair_valid_json_unchanged() {
let input = r#"{"key": "value"}"#;
let repaired = repair_json(input);
assert_eq!(repaired, input);
}
#[test]
fn repair_deeply_truncated() {
let input = r#"{"tools": [{"name": "read_file", "args": {"path": "/tmp"#;
let repaired = repair_json(input);
let val: serde_json::Value = serde_json::from_str(&repaired).unwrap();
assert_eq!(val["tools"][0]["name"], "read_file");
}
#[test]
fn parse_valid_json_fast_path() {
let input = r#"{"a": 1}"#;
let val = parse_with_repair(input).unwrap();
assert_eq!(val["a"], 1);
}
#[test]
fn parse_malformed_json_repair_path() {
let input = "```json\n{\"a\": 1,}\n```";
let val = parse_with_repair(input).unwrap();
assert_eq!(val["a"], 1);
}
#[test]
fn parse_truly_broken_json_fails() {
let input = "this is not json at all";
assert!(parse_with_repair(input).is_err());
}
#[test]
fn parse_empty_object() {
let input = "{}";
let val = parse_with_repair(input).unwrap();
assert!(val.is_object());
}
#[test]
fn parse_fenced_array() {
let input = "```\n[1, 2, 3]\n```";
let val = parse_with_repair(input).unwrap();
assert_eq!(val, serde_json::json!([1, 2, 3]));
}
#[test]
fn escaped_quotes_in_string() {
let input = r#"{"msg": "he said \"hello\""}"#;
let repaired = repair_json(input);
let val: serde_json::Value = serde_json::from_str(&repaired).unwrap();
assert_eq!(val["msg"], r#"he said "hello""#);
}
#[test]
fn empty_input() {
let repaired = repair_json("");
assert_eq!(repaired, "");
}
#[test]
fn just_fences_no_content() {
let input = "```json\n```";
let repaired = repair_json(input);
assert_eq!(repaired, "");
}
#[test]
fn multiple_trailing_commas() {
let input = r#"{"a": 1,, "b": 2,}"#;
let fixed = fix_trailing_commas(input);
assert_eq!(fixed, r#"{"a": 1,, "b": 2}"#);
}
#[test]
fn repair_real_llm_tool_call() {
let input = r#"```json
{
"path": "/home/user/project/src/main.rs",
"content": "fn main() {\n println!(\"Hello, world!\");\n}",
}
```"#;
let val = parse_with_repair(input).unwrap();
assert_eq!(val["path"], "/home/user/project/src/main.rs");
assert!(val["content"].as_str().unwrap().contains("Hello, world!"));
}
#[test]
fn repair_truncated_tool_call() {
let input = r#"{"query": "rust programming", "num_results": 5"#;
let val = parse_with_repair(input).unwrap();
assert_eq!(val["query"], "rust programming");
assert_eq!(val["num_results"], 5);
}
}