From 578bc73f12a97eaa3ca4ccc8d212f2a62a29fcbc Mon Sep 17 00:00:00 2001 From: LHMQ878 <72402929@cityu-dg.edu.cn> Date: Thu, 6 Aug 2026 19:30:22 +0800 Subject: [PATCH] fix(json_schema): keep min/maxLength when format falls back to str _create_string_type returned early for any format, dropping minLength, maxLength, and pattern even when the format resolves to a plain str (custom formats / uri-reference). Fixes #4404 --- .../fastmcp/utilities/json_schema_type.py | 24 ++++++++++++------- .../json_schema_type/test_formats.py | 19 +++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/fastmcp_slim/fastmcp/utilities/json_schema_type.py b/fastmcp_slim/fastmcp/utilities/json_schema_type.py index de1db2900..b3e64ffb5 100644 --- a/fastmcp_slim/fastmcp/utilities/json_schema_type.py +++ b/fastmcp_slim/fastmcp/utilities/json_schema_type.py @@ -269,13 +269,6 @@ def _create_string_type(schema: Mapping[str, Any]) -> type | Annotated[Any, ...] if "const" in schema: return Literal[schema["const"]] # type: ignore - if fmt := schema.get("format"): - if fmt == "uri": - return AnyUrl - elif fmt == "uri-reference": - return str - return FORMAT_TYPES.get(fmt, str) - constraints = { k: v for k, v in { @@ -286,7 +279,22 @@ def _create_string_type(schema: Mapping[str, Any]) -> type | Annotated[Any, ...] if v is not None } - if not constraints: + # Resolve format first. Non-str formats (date-time, email, uri, json) do not + # take length/pattern constraints. Str-backed formats (unknown custom names + # and uri-reference) must still honor minLength/maxLength/pattern. + if fmt := schema.get("format"): + if fmt == "uri": + return AnyUrl + if fmt == "uri-reference": + base: type | Annotated[Any, ...] = str + else: + base = FORMAT_TYPES.get(fmt, str) + if base is not str: + return base + if not constraints: + return str + # Fall through to apply StringConstraints on the str-backed format. + elif not constraints: return str annotated: Any = Annotated[str, StringConstraints(**constraints)] diff --git a/tests/utilities/json_schema_type/test_formats.py b/tests/utilities/json_schema_type/test_formats.py index 781b81a82..b36bafd36 100644 --- a/tests/utilities/json_schema_type/test_formats.py +++ b/tests/utilities/json_schema_type/test_formats.py @@ -112,3 +112,22 @@ class TestFormatTypes: ) assert isinstance(result.full_uri, AnyUrl) assert isinstance(result.ref_uri, str) + + def test_str_backed_format_keeps_max_length(self): + """Unknown/custom formats fall back to str and must still enforce length.""" + t = json_schema_to_type( + {"type": "string", "format": "phone", "maxLength": 3} + ) + validator = TypeAdapter(t) + assert validator.validate_python("123") == "123" + with pytest.raises(ValidationError): + validator.validate_python("abcd") + + def test_uri_reference_format_keeps_min_length(self): + t = json_schema_to_type( + {"type": "string", "format": "uri-reference", "minLength": 2} + ) + validator = TypeAdapter(t) + assert validator.validate_python("/a") == "/a" + with pytest.raises(ValidationError): + validator.validate_python("")