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:
Jeremiah Lowin 2026-04-07 17:47:27 -04:00
commit d69a86dccf
No known key found for this signature in database
2 changed files with 31 additions and 13 deletions

View file

@ -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 {})

View file

@ -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 = {