Nullability and Sentinel Logic
In pydantic-core, handling nullability and permissive types is managed through a set of specialized schemas. These schemas allow you to define how the validator should treat None values, dynamic data, and internal sentinel states.
Permissive Validation with AnySchema
The AnySchema (created via core_schema.any_schema()) is the most permissive schema type in the codebase. It accepts any Python object without modification or validation errors.
This is frequently used as a fallback for unstructured data, such as the values in a dictionary where the types are not known in advance.
from pydantic_core import SchemaValidator, core_schema
# Using any_schema for dynamic dictionary values
schema = core_schema.dict_schema(
keys_schema=core_schema.str_schema(),
values_schema=core_schema.any_schema(),
)
v = SchemaValidator(schema)
assert v.validate_python({'a': 1, 'b': 'hello', 'c': [1, 2, 3]}) == {'a': 1, 'b': 'hello', 'c': [1, 2, 3]}
In serialization, AnySchema is often the default return_schema for function-based serializers when no specific schema is provided, ensuring that the output of the function is passed through without further validation.
Nullability: Nullable vs. None
The codebase makes a clear distinction between a schema that allows None and a schema that requires None.
NullableSchema
The NullableSchema is a wrapper that allows an underlying schema to also accept None (which corresponds to null in JSON). This is the standard way to implement Optional[T] types.
from pydantic_core import SchemaValidator, core_schema
# Allows a string OR None
schema = core_schema.nullable_schema(core_schema.str_schema())
v = SchemaValidator(schema)
assert v.validate_python("hello") == "hello"
assert v.validate_python(None) is None
The NullableSchema includes a strict field. When set, it controls whether the underlying schema should be validated in strict mode, while still allowing None.
NoneSchema
The NoneSchema (created via core_schema.none_schema()) is not a wrapper. It validates that the input is exactly None. If any other value is provided, it raises a ValidationError.
from pydantic_core import SchemaValidator, core_schema
schema = core_schema.none_schema()
v = SchemaValidator(schema)
assert v.validate_python(None) is None
# v.validate_python(1) would raise ValidationError
Recursive Nullability
NullableSchema is essential for defining recursive data structures like trees or linked lists, where a field might point to another instance of the same model or be None to terminate the recursion.
In pydantic-core/tests/serializers/test_definitions_recursive.py, this is demonstrated using definitions_schema and definition_reference_schema:
from pydantic_core import SchemaSerializer, core_schema
schema = core_schema.definitions_schema(
core_schema.definition_reference_schema('Branch'),
[
core_schema.typed_dict_schema(
{
'name': core_schema.typed_dict_field(core_schema.str_schema()),
'sub_branch': core_schema.typed_dict_field(
# Recursively reference 'Branch' but allow it to be None
core_schema.nullable_schema(core_schema.definition_reference_schema('Branch'))
),
},
ref='Branch',
)
],
)
s = SchemaSerializer(schema)
assert s.to_python({'name': 'root', 'sub_branch': None}) == {'name': 'root', 'sub_branch': None}
Sentinel Logic with InvalidSchema
The InvalidSchema is a sentinel type used to represent an invalid or uninitialized schema state. While it exists in the core_schema definitions, it is not intended for use in active validation.
Attempting to construct a SchemaValidator with an InvalidSchema will result in a SchemaError. This is verified in pydantic-core/tests/test_schema_functions.py:
import pytest
from pydantic_core import SchemaValidator, core_schema, SchemaError
def test_err_on_invalid() -> None:
# This will raise: SchemaError: Cannot construct schema with `InvalidSchema` member.
with pytest.raises(SchemaError, match='Cannot construct schema with `InvalidSchema` member.'):
SchemaValidator(core_schema.invalid_schema())
This schema serves as a type-safe placeholder within the CoreSchema union, ensuring that internal logic can handle "invalid" states without resorting to Any or None types that might bypass type checking.