Skip to content

Commit 438a4ab

Browse files
committed
fix(utils): ignore braces inside strings when extracting nested json (#1401)
1 parent 384dfe5 commit 438a4ab

2 files changed

Lines changed: 135 additions & 21 deletions

File tree

‎backend/common/utils/utils.py‎

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,30 +53,23 @@ def deepcopy_ignore_extra(src, dest):
5353
return dest
5454

5555

56-
def extract_nested_json(text):
57-
stack = []
58-
start_index = -1
59-
results = []
56+
def extract_nested_json(text: str) -> Optional[str]:
57+
if not text or not isinstance(text, str):
58+
return None
6059

60+
decoder = json.JSONDecoder(strict=False)
6161
for i, char in enumerate(text):
6262
if char in '{[':
63-
if not stack: # 记录起始位置
64-
start_index = i
65-
stack.append(char)
66-
elif char in '}]':
67-
if stack and ((char == '}' and stack[-1] == '{') or (char == ']' and stack[-1] == '[')):
68-
stack.pop()
69-
if not stack: # 栈空时截取完整JSON
70-
json_str = text[start_index:i + 1]
71-
try:
72-
orjson.loads(json_str) # 验证有效性
73-
results.append(json_str)
74-
except:
75-
pass
76-
else:
77-
stack = [] # 括号不匹配则重置
78-
if len(results) > 0 and results[0]:
79-
return results[0]
63+
try:
64+
_, end = decoder.raw_decode(text, i)
65+
candidate = text[i:end]
66+
try:
67+
orjson.loads(candidate)
68+
return candidate
69+
except Exception:
70+
continue
71+
except Exception:
72+
continue
8073
return None
8174

8275
def string_to_numeric_hash(text: str, bits: Optional[int] = 64) -> int:
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import os
2+
import sys
3+
import pytest
4+
import orjson
5+
6+
# Ensure backend root is in sys.path
7+
backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8+
if backend_dir not in sys.path:
9+
sys.path.insert(0, backend_dir)
10+
11+
from common.utils.utils import extract_nested_json
12+
13+
14+
def test_standard_json_extraction():
15+
raw = '{"sql": "SELECT * FROM users WHERE status = 1", "success": true}'
16+
extracted = extract_nested_json(raw)
17+
assert extracted is not None
18+
data = orjson.loads(extracted)
19+
assert data["sql"] == "SELECT * FROM users WHERE status = 1"
20+
assert data["success"] is True
21+
22+
23+
def test_standard_json_array_extraction():
24+
raw = '[{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]'
25+
extracted = extract_nested_json(raw)
26+
assert extracted is not None
27+
data = orjson.loads(extracted)
28+
assert len(data) == 2
29+
assert data[0]["id"] == 1
30+
31+
32+
def test_sql_with_single_left_brace():
33+
"""Test Issue #1401: SQL containing an unmatched left curly brace '{'."""
34+
raw = """{"sql": "SELECT * FROM config WHERE path LIKE '{'", "success": true}"""
35+
extracted = extract_nested_json(raw)
36+
assert extracted is not None
37+
data = orjson.loads(extracted)
38+
assert data["sql"] == "SELECT * FROM config WHERE path LIKE '{'"
39+
assert data["success"] is True
40+
41+
42+
def test_sql_with_single_right_brace():
43+
"""Test Issue #1401: SQL containing an unmatched right curly brace '}'."""
44+
raw = """{"sql": "SELECT * FROM logs WHERE note LIKE '%}'", "success": true}"""
45+
extracted = extract_nested_json(raw)
46+
assert extracted is not None
47+
data = orjson.loads(extracted)
48+
assert data["sql"] == "SELECT * FROM logs WHERE note LIKE '%}'"
49+
assert data["success"] is True
50+
51+
52+
def test_sql_with_balanced_braces_in_like_pattern():
53+
"""Test Issue #1401: SQL with LIKE pattern containing '{pattern}'."""
54+
raw = """{"sql": "SELECT * FROM users WHERE tag LIKE '%{vip}%'", "success": true}"""
55+
extracted = extract_nested_json(raw)
56+
assert extracted is not None
57+
data = orjson.loads(extracted)
58+
assert data["sql"] == "SELECT * FROM users WHERE tag LIKE '%{vip}%'"
59+
assert data["success"] is True
60+
61+
62+
def test_sql_with_escaped_quotes_and_braces():
63+
"""Test SQL with escaped double quotes and braces inside string literal."""
64+
raw = r"""{"sql": "SELECT \"col\" FROM t WHERE json_val = '{\"key\": \"val\"}'", "success": true}"""
65+
extracted = extract_nested_json(raw)
66+
assert extracted is not None
67+
data = orjson.loads(extracted)
68+
assert data["sql"] == 'SELECT "col" FROM t WHERE json_val = \'{"key": "val"}\''
69+
assert data["success"] is True
70+
71+
72+
def test_sql_with_escaped_backslashes():
73+
"""Test SQL containing escaped backslashes and path delimiters."""
74+
raw = r"""{"sql": "SELECT * FROM files WHERE path = 'C:\\Users\\default\\{tmp}'", "success": true}"""
75+
extracted = extract_nested_json(raw)
76+
assert extracted is not None
77+
data = orjson.loads(extracted)
78+
assert data["sql"] == r"SELECT * FROM files WHERE path = 'C:\Users\default\{tmp}'"
79+
assert data["success"] is True
80+
81+
82+
def test_markdown_code_block_wrapping():
83+
"""Test JSON wrapped inside markdown code block."""
84+
raw = """Here is the query result:
85+
```json
86+
{
87+
"sql": "SELECT * FROM users WHERE template LIKE '%{test}%'",
88+
"success": true
89+
}
90+
```
91+
Hope this helps!"""
92+
extracted = extract_nested_json(raw)
93+
assert extracted is not None
94+
data = orjson.loads(extracted)
95+
assert data["sql"] == "SELECT * FROM users WHERE template LIKE '%{test}%'"
96+
assert data["success"] is True
97+
98+
99+
def test_leading_explanation_with_braces_and_quotes():
100+
"""Test text preceding the JSON that contains braces or quotes."""
101+
raw = """注意:参数模板例如 {workspace_id},大模型返回的结果如下:
102+
{"sql": "SELECT * FROM table WHERE col = '{foo}'", "success": true}"""
103+
extracted = extract_nested_json(raw)
104+
assert extracted is not None
105+
data = orjson.loads(extracted)
106+
assert data["sql"] == "SELECT * FROM table WHERE col = '{foo}'"
107+
assert data["success"] is True
108+
109+
110+
def test_invalid_candidate_before_valid_json():
111+
"""Test that invalid braced block before valid JSON does not break extraction."""
112+
raw = '{invalid json here} Followed by valid: {"sql": "SELECT 1", "success": true}'
113+
extracted = extract_nested_json(raw)
114+
assert extracted is not None
115+
data = orjson.loads(extracted)
116+
assert data["sql"] == "SELECT 1"
117+
118+
119+
@pytest.mark.parametrize("invalid_input", [None, "", " ", 123, [], {}, "No json at all here"])
120+
def test_invalid_or_empty_inputs(invalid_input):
121+
assert extract_nested_json(invalid_input) is None

0 commit comments

Comments
 (0)