From 67f7a5517a5b1ae6a65236cf4ab51209a022e771 Mon Sep 17 00:00:00 2001 From: Harsh Kashyap Date: Tue, 8 Sep 2026 08:32:14 +0000 Subject: [PATCH] fix(api): anchor JSON extraction on brackets in parse_json_markdown (#41959) --- api/libs/json_in_md_parser.py | 26 ++++++++++++++----- .../unit_tests/libs/test_json_in_md_parser.py | 17 ++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/api/libs/json_in_md_parser.py b/api/libs/json_in_md_parser.py index 310e6777478..a4f8b0884f8 100644 --- a/api/libs/json_in_md_parser.py +++ b/api/libs/json_in_md_parser.py @@ -6,23 +6,35 @@ from core.llm_generator.output_parser.errors import OutputParserError def parse_json_markdown(json_string: str): # Get json from the backticks/braces json_string = json_string.strip() - starts = ["```json", "```", "``", "`", "{", "["] - ends = ["```", "``", "`", "}", "]"] + parsed: dict = {} + + # Anchor on the JSON brackets themselves: from the first "{" or "[" to the + # last "}" or "]". This works whether or not the JSON is wrapped in code + # fences, and - unlike marker-priority search - is not confused by + # backticks inside JSON string values or in surrounding prose. + start_candidates = [i for i in (json_string.find("{"), json_string.find("[")) if i != -1] + if start_candidates: + start_index = min(start_candidates) + end_index = max(json_string.rfind("}"), json_string.rfind("]")) + if end_index != -1 and start_index < end_index: + end_index += 1 + extracted_content = json_string[start_index:end_index].strip() + return json.loads(extracted_content) + + # Fallback for fenced content without brackets (e.g. a fenced scalar). + starts = ["```json", "```", "``", "`"] + ends = ["```", "``", "`"] end_index = -1 start_index = 0 - parsed: dict = {} for s in starts: start_index = json_string.find(s) if start_index != -1: - if json_string[start_index] not in ("{", "["): - start_index += len(s) + start_index += len(s) break if start_index != -1: for e in ends: end_index = json_string.rfind(e, start_index) if end_index != -1: - if json_string[end_index] in ("}", "]"): - end_index += 1 break if start_index != -1 and end_index != -1 and start_index < end_index: extracted_content = json_string[start_index:end_index].strip() diff --git a/api/tests/unit_tests/libs/test_json_in_md_parser.py b/api/tests/unit_tests/libs/test_json_in_md_parser.py index 953f203e895..57a45e07669 100644 --- a/api/tests/unit_tests/libs/test_json_in_md_parser.py +++ b/api/tests/unit_tests/libs/test_json_in_md_parser.py @@ -107,3 +107,20 @@ def test_parse_and_check_json_markdown_handles_think_fenced_and_raw_variants(): for src in cases: obj = parse_and_check_json_markdown(src, ["keywords", "category_id", "category_name"]) assert obj == expected + + +def test_parse_json_markdown_backtick_inside_string_value(): + """Backticks inside JSON string values must not be mistaken for code fences.""" + src = '{"code": "use `print` function", "n": 1}' + assert parse_json_markdown(src) == {"code": "use `print` function", "n": 1} + + +def test_parse_json_markdown_backtick_in_surrounding_prose(): + """Backticks in prose before the JSON must not break extraction.""" + src = 'Here is `the` result: {"a": 1}' + assert parse_json_markdown(src) == {"a": 1} + + +def test_parse_json_markdown_fenced_scalar_still_supported(): + """Fenced content without brackets still parses via the fence fallback.""" + assert parse_json_markdown('```json\n"hello"\n```') == "hello"