Skip to content

fix(types): JSON-escape literal strings inside container types - #2045

Open
BlueX888 wants to merge 1 commit into
dottxt-ai:mainfrom
BlueX888:fix/prep-literal-json-escape-in-containers
Open

BlueX888 wants to merge 1 commit into
dottxt-ai:mainfrom
BlueX888:fix/prep-literal-json-escape-in-containers

Conversation

@BlueX888

Copy link
Copy Markdown

🚧 Thank you for opening a PR!

  • We should be able to understand what the PR does from its title only;
  • There is a high-level description of the changes;
  • If I add a new feature, there is an issue discussing it already; — bug fix, no feature
  • There are links to all the relevant issues, discussions and PRs;
  • The branch is rebased on the latest main commit;
  • Commit messages follow these guidelines;
  • One commit per logical change;
  • The code respects the current naming conventions;
  • Docstrings follow the numpy style guide;
  • pre-commit is installed and configured on your machine, and you ran it before opening the PR;
  • There are tests covering the changes;
  • The documentation is up-to-date; — no documented behaviour changes

Summary

_ensure_json_quoted now encodes String values with json.dumps instead of
wrapping them in quote characters by hand, so a literal containing ", \ or
a control character produces the escape sequence JSON requires.

Why

_ensure_json_quoted (src/outlines/types/dsl.py:880) built the quoted form as
String(f'"{term.value}"'). That adds the delimiters but never escapes the
value, so the generated regex only matched the unescaped text. For
list[Literal['say "hi"']] the pattern came out as:

\[("say\ "hi"")(,\ ("say\ "hi""))*\]

re.fullmatch on ["say \"hi\""] — the encoding json.dumps produces, and the
only form a JSON parser accepts — returns None. The pattern instead accepts
["say "hi""], which no parser can read. dict[Literal['say "hi"'], str] is
wrong the same way for its keys, and Tuple[Literal[...]] for its elements,
since all three go through the same helper.

A literal containing a backslash, a tab or a newline is in the same position:
_json_string_builtin_patterns in #2015 and the corresponding section of #1962
already note that these values need escaping on top of quoting; that PR scopes
itself to built-in Regex terms and leaves the String branch untouched, so
this is the other half.

Changes

  • src/outlines/types/dsl.py: _ensure_json_quoted returns
    String(json.dumps(term.value, ensure_ascii=False)).
  • tests/types/test_dsl.py: two tests covering List, Dict and Tuple.

ensure_ascii=False keeps non-ASCII characters literal, matching the previous
output and the outlines_core JSON-Schema path; json.dumps with ASCII values
reproduces the old string exactly, so nothing outside the " / \ /
control-character cases changes.

Testing

$ .venv/bin/python -m pytest tests/types/test_dsl.py -k "json_escapes"   # before the fix
FAILED tests/types/test_dsl.py::test_e2e_list_literal_requires_json_escapes
FAILED tests/types/test_dsl.py::test_e2e_dict_and_tuple_literal_require_json_escapes
2 failed, 60 deselected

$ .venv/bin/python -m pytest tests/types/test_dsl.py -k "json_escapes"   # after
2 passed, 60 deselected

$ .venv/bin/python -m pytest tests/types/
348 passed in 0.93s

$ .venv/bin/pre-commit run --all-files
check for merge conflicts...................Passed
debug statements (python)...................Passed
fix end of files............................Passed
trim trailing whitespace....................Passed
mypy........................................Passed
ruff........................................Passed

No issue is filed for this; it is the String half of the escaping gap
described in #1962, tracked there and explicitly left open by #2015.

`_ensure_json_quoted` wrapped `String` values in quote characters by hand, so
values containing `"`, `\` or control characters were quoted but not escaped.
The generated regex then only matched the unescaped text: `List[Literal['say
"hi"']]` accepted `["say "hi""]`, which no JSON parser can read, and rejected
`["say \"hi\""]`, the encoding `json.dumps` produces. Dict keys/values and
Tuple elements use the same helper.

Encode the value with `json.dumps(..., ensure_ascii=False)` so escapes match
JSON (`ensure_ascii=False` keeps non-ASCII characters literal, as the
`outlines_core` JSON-Schema path and the previous behaviour both do).

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.

1 participant