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")