diff --git a/api/libs/json_in_md_parser.py b/api/libs/json_in_md_parser.py index a59fd1eb805798..1619e79928b11d 100644 --- a/api/libs/json_in_md_parser.py +++ b/api/libs/json_in_md_parser.py @@ -56,6 +56,12 @@ def parse_and_check_json_markdown(text: str, expected_keys: list[str]): json_obj = json_obj[0] else: raise OutputParserError(f"got invalid return object. obj:{json_obj}") + if not isinstance(json_obj, dict): + # Scalars or plain strings (e.g. a fenced number/string) are not a + # valid return object. Without this check, `key not in json_obj` + # raises TypeError for numbers/None/bools and silently does a + # substring match for strings. + raise OutputParserError(f"got invalid return object. obj:{json_obj}") for key in expected_keys: if key not in json_obj: raise OutputParserError( 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 48b6f749a83a9c..9535a6e72f5f26 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 @@ -124,3 +124,16 @@ def test_parse_json_markdown_backtick_in_surrounding_prose() -> None: def test_parse_json_markdown_fenced_scalar_still_supported() -> None: """Fenced content without brackets still parses via the fence fallback.""" assert parse_json_markdown('```json\n"hello"\n```') == "hello" + + +def test_parse_and_check_json_markdown_fenced_scalar_raises_output_parser_error() -> None: + """A valid JSON scalar (number/null/bool) must raise OutputParserError, not TypeError.""" + for src in ("```json\n42\n```", "```json\nnull\n```", "```json\ntrue\n```"): + with pytest.raises(OutputParserError): + parse_and_check_json_markdown(src, ["answer"]) + + +def test_parse_and_check_json_markdown_rejects_non_dict_object() -> None: + """A JSON string that merely contains an expected key as a substring must not pass.""" + with pytest.raises(OutputParserError): + parse_and_check_json_markdown('```json\n"the generated prompt text"\n```', ["prompt"])