fix: reject string schemas that mix pattern/format with length bounds (#49996)

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

Yufeng He committed Aug 19, 2026 at 17:29 UTC 340b7e4909f14ff33943ab6ac7955191e1b8b4ff
2 files changed +20
tests/v1/structured_output/test_utils.py
+6
@@ -14,6 +14,12 @@ pytestmark = pytest.mark.cpu_test
14 def unsupported_string_schemas():
15 return [
16 {"type": "string", "format": "non_existing_format"},
17 + # pattern/format is compiled but length bounds are silently dropped,
18 + # so the combination must be rejected instead of producing quietly
19 + # wrong output
20 + {"type": "string", "pattern": "^a+$", "maxLength": 2},
21 + {"type": "string", "pattern": "^a+$", "minLength": 3},
22 + {"type": "string", "format": "email", "maxLength": 10},
23 ]
24
25
vllm/v1/structured_output/backend_xgrammar.py
+14
@@ -262,6 +262,20 @@ def has_xgrammar_unsupported_json_features(schema: dict[str, Any]) -> bool:
262 ):
263 return True
264
265 + # A string mixing a generative constraint (pattern or format) with
266 + # explicit length bounds. xgrammar compiles the pattern/format side
267 + # and silently drops minLength/maxLength from the grammar, so output
268 + # can violate the bound without any error surfacing. Verified against
269 + # the compiled EBNF: pattern/format grammars come out byte-identical
270 + # with and without the length keywords, while maxLength alone lowers
271 + # to {0, N} correctly.
272 + if (
273 + obj.get("type") == "string"
274 + and ("pattern" in obj or "format" in obj)
275 + and ("minLength" in obj or "maxLength" in obj)
276 + ):
277 + return True
278 +
279 # Unsupported keywords for objects
280 if obj.get("type") == "object" and any(
281 key in obj for key in ("patternProperties", "propertyNames")