Skip to main content

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.