Skip to content

fix(utils): keep the payload when markdown text contains backticks - #381

Open
mosifu wants to merge 2 commits into
langgenius:mainfrom
mosifu:fix/json-in-markdown-trailing-content
Open

mosifu wants to merge 2 commits into
langgenius:mainfrom
mosifu:fix/json-in-markdown-trailing-content

Conversation

@mosifu

@mosifu mosifu commented Nov 2, 2026

Copy link
Copy Markdown

Symptom

parse_json_markdown rejects a valid payload when a backtick appears anywhere after it — an inline code span is enough:

parse_json_markdown('{"a": 1} and `code`')
# JSONDecodeError: Expecting value: line 1 column 1 (char 0)

parse_json_markdown('{"a": 1} then a lone ` backtick')
# ValueError: could not find json block in the output.

The same payload alone parses fine, so the failure depends purely on unrelated text elsewhere in the response.

Fixes #380

Root cause

Two independent defects, both from slicing by text search instead of parsing:

1. Start offset is chosen by marker kind, not by position. The loop

for start_marker in ["```json", "```", "``", "`", "{", "["]:
    start_index = json_string.find(start_marker)

keeps the first marker kind that occurs anywhere, and ` is tried before {. A backtick later in the text therefore wins over a brace that appears earlier, and the offset lands on the backtick — the payload is never considered.

2. End offset takes the last closing character. rfind(end_marker, start_index) scans to the end of the string, so two payloads (or a stray } in trailing prose) make the slice run past the first value and json.loads fail on the extra data:

parse_json_markdown('{"a": 1} then {"b": 2}')
# JSONDecodeError: Extra data: line 1 column 10 (char 9)

Fix

  • _payload_start prefers braces over backticks (fences keep priority, so a fenced block still beats prose braces before it), and skips whitespace after a fence marker to land on the payload itself.
  • The payload is then read with json.JSONDecoder().raw_decode, which parses exactly one value and stops: trailing prose, a second payload, or a } inside a string can no longer extend the slice.
value, _end = json.JSONDecoder().raw_decode(text, start_index)

No change to the error contract: a missing payload still raises ValueError("could not find json block in the output."), and malformed JSON still raises JSONDecodeError, so parse_and_check_json_markdown keeps wrapping it into OutputParserError.

Scope

'[{"a": 1}]' still returns {"a": 1} — the brace is found before the bracket, and test_parse_json_markdown_extracts_raw_json_array asserts exactly that. Whether a bare top-level array should be returned as a list is a separate contract question; this PR does not touch it.

Tests

Five cases added to tests/utils/test_json_in_md_parser.py:

Test Asserts
..._ignores_a_backtick_after_the_payload '{"a": 1} and code' → {"a": 1}
..._ignores_a_lone_backtick_after_the_payload previously ValueError, now parses
..._ignores_an_inline_code_span_before_the_payload 'Use code then {"a": 1}' → {"a": 1}
..._returns_the_first_of_two_payloads first value wins, no "Extra data"
..._keeps_braces_inside_strings '{"a": "```"}' → {"a": "```"}

All seven pre-existing tests are unchanged and still pass.

Verification

The module is stdlib-only (json), so I loaded it by path and ran the seven existing assertions plus the new cases as a standalone harness: 15/15 pass. I did not run the repo's pytest suite locally — importing graphon.utils pulls the package __init__, which needs the full dependency set (transformers, pandas, unstructured) that is not installed here — so CI will be the first pytest run. The assertions themselves are reproduced one-for-one in the harness, including the OutputParserError wrapping cases.

parse_json_markdown picked the first marker *kind* that occurred anywhere in
the text, with backticks ahead of braces, and closed the slice at the last
end marker via rfind. A single backtick after a valid payload therefore
hijacked the start offset, and the valid JSON at offset 0 was reported as
"could not find json block". Two payloads in one response failed the same way.

Find the payload start by preferring braces over backticks, and let the JSON
decoder parse exactly one value instead of slicing to the last end marker.
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

All contributors on this pull request have signed the CLA.
Posted by the CLA Assistant Lite bot.

@mosifu

mosifu commented Nov 3, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(utils): parse_json_markdown drops a valid payload when the text contains a backtick

1 participant