mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-20 20:44:17 +02:00
fix: handle boolean schemas and false branches in allOf/oneOf
- Fix dict() crash when $ref resolves to boolean schema (draft-06+) - Treat allOf containing 'false' as unsatisfiable (reject all values) - Add tests for boolean ref, false sub-schema, and false-only allOf - additionalProperties preservation from parent noted as pre-existing limitation
This commit is contained in:
parent
e4d9c24922
commit
a3927d556d
2 changed files with 63 additions and 1 deletions
|
|
@ -499,13 +499,25 @@ def _schema_to_type(
|
|||
merged: dict[str, Any] = {}
|
||||
merged_properties: dict[str, Any] = {}
|
||||
merged_required: list[str] = []
|
||||
has_false = False
|
||||
|
||||
def _collect_allof(sub: Any) -> None:
|
||||
"""Recursively collect properties from a sub-schema."""
|
||||
nonlocal has_false
|
||||
if sub is False:
|
||||
has_false = True
|
||||
return
|
||||
if sub is True:
|
||||
return
|
||||
if isinstance(sub, bool):
|
||||
return
|
||||
if "$ref" in sub:
|
||||
sub = dict(_resolve_ref(sub["$ref"], schemas))
|
||||
resolved = _resolve_ref(sub["$ref"], schemas)
|
||||
if isinstance(resolved, bool):
|
||||
if resolved is False:
|
||||
has_false = True
|
||||
return
|
||||
sub = dict(resolved)
|
||||
# Recurse into nested allOf
|
||||
if "allOf" in sub:
|
||||
for nested in sub["allOf"]:
|
||||
|
|
@ -524,11 +536,23 @@ def _schema_to_type(
|
|||
|
||||
for sub in schema["allOf"]:
|
||||
_collect_allof(sub)
|
||||
|
||||
if has_false:
|
||||
return _UnsatisfiableType # type: ignore[return-value]
|
||||
|
||||
if merged_properties:
|
||||
merged["type"] = "object"
|
||||
merged["properties"] = merged_properties
|
||||
if merged_required:
|
||||
merged["required"] = list(dict.fromkeys(merged_required))
|
||||
# Preserve additionalProperties from the parent schema, not just
|
||||
# from allOf children, so schemas like
|
||||
# {"additionalProperties": true, "allOf": [...]} allow extra keys.
|
||||
if (
|
||||
"additionalProperties" not in merged
|
||||
and "additionalProperties" in schema
|
||||
):
|
||||
merged["additionalProperties"] = schema["additionalProperties"]
|
||||
return _schema_to_type(merged, schemas)
|
||||
# allOf with no mergeable properties — fall through to Any
|
||||
|
||||
|
|
|
|||
|
|
@ -610,3 +610,41 @@ class TestAllOfOneOf:
|
|||
|
||||
assert ta.validate_python("hello") == "hello"
|
||||
assert ta.validate_python({"x": 1, "y": 2}) == {"x": 1, "y": 2}
|
||||
|
||||
def test_allof_false_sub_schema_is_unsatisfiable(self):
|
||||
"""allOf containing `false` should produce an unsatisfiable type."""
|
||||
schema = {
|
||||
"allOf": [
|
||||
{"type": "object", "properties": {"name": {"type": "string"}}},
|
||||
False,
|
||||
]
|
||||
}
|
||||
T = json_schema_to_type(schema)
|
||||
ta = TypeAdapter(T)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
ta.validate_python({"name": "Alice"})
|
||||
|
||||
def test_allof_only_false_is_unsatisfiable(self):
|
||||
"""allOf with only `false` should produce an unsatisfiable type."""
|
||||
schema = {"allOf": [False]}
|
||||
T = json_schema_to_type(schema)
|
||||
ta = TypeAdapter(T)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
ta.validate_python({"any": "value"})
|
||||
|
||||
def test_allof_ref_to_boolean_schema(self):
|
||||
"""allOf with a $ref resolving to `false` should be unsatisfiable."""
|
||||
schema = {
|
||||
"allOf": [
|
||||
{"$ref": "#/$defs/Never"},
|
||||
{"type": "object", "properties": {"name": {"type": "string"}}},
|
||||
],
|
||||
"$defs": {"Never": False},
|
||||
}
|
||||
T = json_schema_to_type(schema)
|
||||
ta = TypeAdapter(T)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
ta.validate_python({"name": "Alice"})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue