mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-20 12:34:17 +02:00
fix: enforce false boolean schemas as unsatisfiable
`true` returns Any (any value valid), but `false` now returns an unsatisfiable type that rejects all values during Pydantic validation.
This commit is contained in:
parent
ff8872f2c9
commit
d69a86dccf
2 changed files with 31 additions and 13 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,
|
||||
|
|
@ -319,9 +329,11 @@ def _schema_to_type(
|
|||
"""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".
|
||||
if isinstance(schema, bool):
|
||||
# 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
|
||||
|
|
@ -481,11 +493,13 @@ def _create_pydantic_model(
|
|||
defaults = {}
|
||||
|
||||
for prop_name, prop_schema in properties.items():
|
||||
# Normalize boolean schemas (JSON Schema draft-06+)
|
||||
# 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 = {}
|
||||
|
||||
field_type = _schema_to_type(prop_schema, schemas or {})
|
||||
else:
|
||||
field_type = _schema_to_type(prop_schema, schemas or {})
|
||||
|
||||
# Handle defaults
|
||||
default_value = prop_schema.get("default", MISSING)
|
||||
|
|
@ -548,14 +562,15 @@ def _create_dataclass(
|
|||
|
||||
fields: list[tuple[Any, ...]] = []
|
||||
for prop_name, prop_schema in properties.items():
|
||||
# Normalize boolean schemas (JSON Schema draft-06+)
|
||||
if isinstance(prop_schema, bool):
|
||||
prop_schema = {}
|
||||
|
||||
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 {})
|
||||
|
|
|
|||
|
|
@ -127,8 +127,8 @@ class TestBooleanSchemas:
|
|||
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_does_not_crash(self):
|
||||
"""A property with schema `false` should not crash parsing."""
|
||||
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},
|
||||
|
|
@ -139,6 +139,9 @@ class TestBooleanSchemas:
|
|||
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 = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue