Repository navigation
fix: preserve trailing whitespace in partial JSON strings #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,8 @@ def fix(json_string: str, allow_partial: Union[Allow, int] = ALL): | |
|
|
||
| def _fix(json_string: str, allow: Allow, is_top_level=False): | ||
| try: | ||
| result = complete_any(json_string.strip(), allow, is_top_level) | ||
| # Trailing whitespace can belong to an unfinished string. | ||
| result = complete_any(json_string.lstrip(), allow, is_top_level) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High · Leading whitespace truncates strings When valid leading JSON whitespace precedes an unfinished string, Adjust the returned index for the stripped leading whitespace before Prompt for AI agentsIn `src/partial_json_parser/core/complete.py` at line 21:
**Leading whitespace truncates strings**
When valid leading JSON whitespace precedes an unfinished string, `complete_any` returns an index relative to the `lstrip()` result, but `_fix` applies it to the original `json_string`; the returned head loses part of the string and callers receive a truncated value.
Adjust the returned index for the stripped leading whitespace before `_fix` slices the original string.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Unfinished strings fail parsing When an unfinished string ends with an unescaped tab or newline, or a backslash followed by whitespace, Handle invalid trailing whitespace and dangling escapes in Prompt for AI agentsIn `src/partial_json_parser/core/complete.py` at line 21:
**Unfinished strings fail parsing**
When an unfinished string ends with an unescaped tab or newline, or a backslash followed by whitespace, `_fix` preserves the invalid suffix and `complete_str` appends a quote without validating it; `parse_json` passes invalid JSON to its parser, which raises `JSONDecodeError` instead of returning a completed value.
Handle invalid trailing whitespace and dangling escapes in `complete_str` so closing the string produces valid JSON.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Trailing spaces break partial escapes When an unfinished string ends with a truncated Handle the incomplete escape separately from subsequent string content so recovery does not discard trailing spaces or emit an invalid escape. Prompt for AI agentsIn `src/partial_json_parser/core/complete.py` at line 21:
**Trailing spaces break partial escapes**
When an unfinished string ends with a truncated `\u`, `\U`, or `\x` escape followed by whitespace, `complete_str` searches for an incomplete `\u`, `\U`, or `\x` escape only within a short suffix. Retained spaces can push the escape outside that search, so it appends a quote after invalid JSON and parsing fails; when the escape is still found, it truncates the escape and the following spaces too.
Handle the incomplete escape separately from subsequent string content so recovery does not discard trailing spaces or emit an invalid escape. |
||
| if result is False: | ||
| raise PartialJSON | ||
|
|
||
|
|
@@ -45,14 +46,17 @@ def complete_any(json_string: str, allow: Allow, is_top_level=False) -> Complete | |
| return complete_str(json_string, allow) | ||
|
|
||
| if char in "1234567890": | ||
| return complete_num(json_string, allow, is_top_level) | ||
| return complete_num(json_string.rstrip(), allow, is_top_level) | ||
|
|
||
| if char == "[": | ||
| return complete_arr(json_string, allow) | ||
|
|
||
| if char == "{": | ||
| return complete_obj(json_string, allow) | ||
|
|
||
| # Literal recovery does not need to preserve string content. | ||
| json_string = json_string.rstrip() | ||
|
|
||
| if json_string.startswith("null"): | ||
| return (4, True) | ||
| if "null".startswith(json_string): | ||
|
|
@@ -122,18 +126,27 @@ def not_escaped(index: int): | |
| count = index - len(text_before.rstrip("\\")) | ||
| return count % 2 == 0 | ||
|
|
||
| # Keep the existing recovery of incomplete escapes and control suffixes. | ||
| trimmed_end = len(json_string.rstrip()) | ||
| escape_end = min(i, trimmed_end) | ||
| if escape_end and json_string[escape_end - 1] == "\\" and not_escaped(escape_end - 1): | ||
| escape_end -= 1 | ||
| i = escape_end | ||
| elif any(ord(char) < 32 for char in json_string[trimmed_end:]): | ||
| i = trimmed_end | ||
|
|
||
| # \uXXXX | ||
| _u = json_string.rfind("\\u", max(0, i - 5), i) | ||
| _u = json_string.rfind("\\u", max(0, escape_end - 5), escape_end) | ||
| if _u != -1 and not_escaped(_u): | ||
| return _u, '"' | ||
|
|
||
| # \UXXXXXXXX | ||
| _U = json_string.rfind("\\U", max(0, i - 9), i) | ||
| _U = json_string.rfind("\\U", max(0, escape_end - 9), escape_end) | ||
| if _U != -1 and not_escaped(_U): | ||
| return _U, '"' | ||
|
|
||
| # \xXX | ||
| _x = json_string.rfind("\\x", max(0, i - 3), i) | ||
| _x = json_string.rfind("\\x", max(0, escape_end - 3), escape_end) | ||
| if _x != -1 and not_escaped(_x): | ||
| return _x, '"' | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.