mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 05:24:18 +02:00
Fix boolean property schemas in JSON Schema parsing (#3785)
This commit is contained in:
parent
3ef9130269
commit
e5b96343d1
2 changed files with 108 additions and 4 deletions
|
|
@ -53,6 +53,7 @@ from typing import (
|
|||
from pydantic import (
|
||||
AnyUrl,
|
||||
BaseModel,
|
||||
BeforeValidator,
|
||||
ConfigDict,
|
||||
EmailStr,
|
||||
Field,
|
||||
|
|
@ -65,6 +66,15 @@ from typing_extensions import NotRequired, TypedDict
|
|||
__all__ = ["JSONSchema", "json_schema_to_type"]
|
||||
|
||||
|
||||
def _reject_all(v: Any) -> Any:
|
||||
"""Validator that rejects every value, implementing JSON Schema `false`."""
|
||||
raise ValueError("No value is valid against a false schema")
|
||||
|
||||
|
||||
# JSON Schema `false` means no value is valid. This type rejects everything
|
||||
# during Pydantic validation.
|
||||
_UnsatisfiableType = Annotated[Any, BeforeValidator(_reject_all)]
|
||||
|
||||
FORMAT_TYPES: dict[str, Any] = {
|
||||
"date-time": datetime,
|
||||
"email": EmailStr,
|
||||
|
|
@ -313,10 +323,18 @@ def _get_from_type_handler(
|
|||
|
||||
|
||||
def _schema_to_type(
|
||||
schema: Mapping[str, Any],
|
||||
schema: Mapping[str, Any] | bool,
|
||||
schemas: Mapping[str, Any],
|
||||
) -> type | ForwardRef:
|
||||
"""Convert schema to appropriate Python type."""
|
||||
# Boolean schemas are valid in JSON Schema draft-06+:
|
||||
# true means "any value is valid" (equivalent to {}),
|
||||
# false means "no value is valid" (unsatisfiable).
|
||||
if schema is True:
|
||||
return Any
|
||||
if schema is False:
|
||||
return _UnsatisfiableType # type: ignore[return-value] # ty:ignore[invalid-return-type]
|
||||
|
||||
if not schema:
|
||||
return object
|
||||
|
||||
|
|
@ -475,7 +493,13 @@ def _create_pydantic_model(
|
|||
defaults = {}
|
||||
|
||||
for prop_name, prop_schema in properties.items():
|
||||
field_type = _schema_to_type(prop_schema, schemas or {})
|
||||
# Boolean schemas (JSON Schema draft-06+): resolve type directly,
|
||||
# then use an empty dict for .get() calls below.
|
||||
if isinstance(prop_schema, bool):
|
||||
field_type = _schema_to_type(prop_schema, schemas or {})
|
||||
prop_schema = {}
|
||||
else:
|
||||
field_type = _schema_to_type(prop_schema, schemas or {})
|
||||
|
||||
# Handle defaults
|
||||
default_value = prop_schema.get("default", MISSING)
|
||||
|
|
@ -540,8 +564,13 @@ def _create_dataclass(
|
|||
for prop_name, prop_schema in properties.items():
|
||||
field_name = _sanitize_name(prop_name)
|
||||
|
||||
# Check for self-reference in property
|
||||
if prop_schema.get("$ref") == "#":
|
||||
# Boolean schemas (JSON Schema draft-06+): resolve type directly,
|
||||
# then use an empty dict for .get() calls below.
|
||||
if isinstance(prop_schema, bool):
|
||||
field_type = _schema_to_type(prop_schema, schemas or {})
|
||||
prop_schema = {}
|
||||
elif prop_schema.get("$ref") == "#":
|
||||
# Check for self-reference in property
|
||||
field_type = ForwardRef(sanitized_name)
|
||||
else:
|
||||
field_type = _schema_to_type(prop_schema, schemas or {})
|
||||
|
|
@ -623,6 +652,10 @@ def _merge_defaults(
|
|||
|
||||
# For each property in the schema
|
||||
for prop_name, prop_schema in schema.get("properties", {}).items():
|
||||
# Normalize boolean schemas (JSON Schema draft-06+)
|
||||
if isinstance(prop_schema, bool):
|
||||
continue
|
||||
|
||||
# If property is missing, apply defaults in priority order
|
||||
if prop_name not in result:
|
||||
if parent_default and prop_name in parent_default:
|
||||
|
|
|
|||
|
|
@ -111,6 +111,77 @@ class TestSimpleTypes:
|
|||
validator.validate_python(False)
|
||||
|
||||
|
||||
class TestBooleanSchemas:
|
||||
"""JSON Schema draft-06+ allows true/false as property schemas."""
|
||||
|
||||
def test_true_property_schema_accepts_any_value(self):
|
||||
"""A property with schema `true` should accept any value."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}, "anything": True},
|
||||
"required": ["name", "anything"],
|
||||
}
|
||||
result = json_schema_to_type(schema)
|
||||
validator = TypeAdapter(result)
|
||||
obj = validator.validate_python({"name": "test", "anything": 42})
|
||||
assert obj.name == "test" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
assert obj.anything == 42 # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
|
||||
def test_false_property_schema_rejects_values(self):
|
||||
"""A property with schema `false` should reject any provided value."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}, "never": False},
|
||||
"required": ["name"],
|
||||
}
|
||||
result = json_schema_to_type(schema)
|
||||
validator = TypeAdapter(result)
|
||||
obj = validator.validate_python({"name": "test"})
|
||||
assert obj.name == "test" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate_python({"name": "test", "never": "anything"})
|
||||
|
||||
def test_boolean_schema_in_object_with_additional_properties(self):
|
||||
"""Boolean property schemas work alongside additionalProperties=True."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"known": {"type": "string"},
|
||||
"flexible": True,
|
||||
},
|
||||
"required": ["known"],
|
||||
"additionalProperties": True,
|
||||
}
|
||||
result = json_schema_to_type(schema)
|
||||
validator = TypeAdapter(result)
|
||||
obj = validator.validate_python(
|
||||
{"known": "hello", "flexible": [1, 2, 3], "extra": "field"}
|
||||
)
|
||||
assert obj.known == "hello" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
assert obj.flexible == [1, 2, 3] # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
|
||||
def test_issue_3783_boolean_property_schemas(self):
|
||||
"""Regression test for GitHub issue #3783."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ts": {"type": "integer"},
|
||||
"level": True,
|
||||
"app": True,
|
||||
"tag": {"type": ["array", "null"], "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["ts"],
|
||||
"additionalProperties": True,
|
||||
}
|
||||
result = json_schema_to_type(schema)
|
||||
validator = TypeAdapter(result)
|
||||
obj = validator.validate_python({"ts": 123, "level": "info", "app": "myapp"})
|
||||
assert obj.ts == 123 # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
assert obj.level == "info" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
assert obj.app == "myapp" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
|
||||
|
||||
class TestConstrainedTypes:
|
||||
def test_constant(self):
|
||||
validator = TypeAdapter(Literal["x"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue