mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-21 13:04:18 +02:00
fix(json-schema): keep string constraints when a format is set
_create_string_type returned early whenever a format was present, so minLength/maxLength/pattern were silently dropped for any string schema that also had a format — including a custom/unknown format (which falls back to str) and uri-reference. Apply the string constraints when the format resolves to a str-based type; non-str formats (date-time, json) are left unchanged.
This commit is contained in:
parent
5de15e0c21
commit
59cb9f50f2
2 changed files with 50 additions and 7 deletions
|
|
@ -269,12 +269,14 @@ def _create_string_type(schema: Mapping[str, Any]) -> type | Annotated[Any, ...]
|
|||
if "const" in schema:
|
||||
return Literal[schema["const"]] # type: ignore
|
||||
|
||||
base: Any = str
|
||||
if fmt := schema.get("format"):
|
||||
if fmt == "uri":
|
||||
return AnyUrl
|
||||
elif fmt == "uri-reference":
|
||||
return str
|
||||
return FORMAT_TYPES.get(fmt, str)
|
||||
base = str
|
||||
else:
|
||||
base = FORMAT_TYPES.get(fmt, str)
|
||||
|
||||
constraints = {
|
||||
k: v
|
||||
|
|
@ -286,10 +288,15 @@ def _create_string_type(schema: Mapping[str, Any]) -> type | Annotated[Any, ...]
|
|||
if v is not None
|
||||
}
|
||||
|
||||
if not constraints:
|
||||
return str
|
||||
# StringConstraints (min/max length, pattern) only apply to str-based types.
|
||||
# Non-str formats (e.g. date-time -> datetime, json -> Json) carry no such
|
||||
# constraints. Previously *any* format made this function return early,
|
||||
# silently dropping minLength/maxLength/pattern even for plain-string
|
||||
# formats (e.g. a custom format that falls back to str).
|
||||
if not constraints or not (isinstance(base, type) and issubclass(base, str)):
|
||||
return base
|
||||
|
||||
annotated: Any = Annotated[str, StringConstraints(**constraints)]
|
||||
annotated: Any = Annotated[base, StringConstraints(**constraints)]
|
||||
|
||||
if "pattern" in constraints:
|
||||
try:
|
||||
|
|
@ -307,10 +314,10 @@ def _create_string_type(schema: Mapping[str, Any]) -> type | Annotated[Any, ...]
|
|||
pattern_field = Field(json_schema_extra={"x-unsupported-pattern": pattern})
|
||||
if constraints:
|
||||
annotated = Annotated[
|
||||
str, StringConstraints(**constraints), pattern_field
|
||||
base, StringConstraints(**constraints), pattern_field
|
||||
] # type: ignore[valid-type]
|
||||
else:
|
||||
annotated = Annotated[str, pattern_field] # type: ignore[valid-type]
|
||||
annotated = Annotated[base, pattern_field] # type: ignore[valid-type]
|
||||
|
||||
return annotated
|
||||
|
||||
|
|
|
|||
|
|
@ -130,3 +130,39 @@ class TestNumberConstraints:
|
|||
validator = TypeAdapter(exclusive_max_number)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate_python(100)
|
||||
|
||||
|
||||
class TestStringFormatConstraints:
|
||||
"""A ``format`` must not silently drop string length/pattern constraints."""
|
||||
|
||||
def test_custom_format_keeps_max_length(self):
|
||||
# An unrecognized format falls back to ``str``; maxLength must still apply.
|
||||
t = json_schema_to_type({"type": "string", "format": "phone", "maxLength": 3})
|
||||
validator = TypeAdapter(t)
|
||||
assert validator.validate_python("abc") == "abc"
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate_python("abcd")
|
||||
|
||||
def test_custom_format_keeps_min_length(self):
|
||||
t = json_schema_to_type({"type": "string", "format": "phone", "minLength": 3})
|
||||
validator = TypeAdapter(t)
|
||||
assert validator.validate_python("abc") == "abc"
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate_python("ab")
|
||||
|
||||
def test_uri_reference_format_keeps_max_length(self):
|
||||
t = json_schema_to_type(
|
||||
{"type": "string", "format": "uri-reference", "maxLength": 5}
|
||||
)
|
||||
validator = TypeAdapter(t)
|
||||
assert validator.validate_python("a/b") == "a/b"
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate_python("too/long/path")
|
||||
|
||||
def test_non_string_format_ignores_length(self):
|
||||
# date-time resolves to ``datetime`` (not str), so length constraints do
|
||||
# not apply and the type must stay datetime (regression guard).
|
||||
import datetime
|
||||
|
||||
t = json_schema_to_type({"type": "string", "format": "date-time"})
|
||||
assert t is datetime.datetime
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue