Boolean, None, and Any Types
In pydantic-core, the handling of basic types like booleans, null values, and arbitrary data is governed by a set of specialized schemas. These schemas define not only how values are validated but also how they are coerced in "lax" mode and how they are transformed during serialization.
Handling Absence and Nullability
The system distinguishes between a literal None value and the concept of a field being "nullable."
None Schema
The NoneSchema is the most restrictive, accepting only the Python None value (or null in JSON). It is defined in pydantic_core/core_schema.py and is typically used when a value must be explicitly null.
Nullable Schema
The NullableSchema acts as a wrapper around another schema, allowing it to accept None in addition to whatever the inner schema validates. This "decorator" pattern is preferred over a general union with None for performance and clarity.
from pydantic_core import SchemaValidator, core_schema
# A schema that accepts an integer OR None
v = SchemaValidator(core_schema.nullable_schema(schema=core_schema.int_schema()))
assert v.validate_python(None) is None
assert v.validate_python(1) == 1
assert v.validate_python('123') == 123
As seen in pydantic-core/tests/validators/test_nullable.py, the NullableSchema ensures that if the input is None, validation succeeds immediately without invoking the inner validator (e.g., the int_schema in the example above).
Boolean Validation and Coercion
The BoolSchema handles boolean validation with a significant distinction between "strict" and "lax" modes. This design allows pydantic-core to be used in environments where data types are often stringified (like HTML forms or environment variables).
Lax Mode Coercion
In the default lax mode, BoolSchema performs broad coercion. It interprets the following as True:
- The boolean
True - The strings
'true','yes','on','1' - The integers
1and1.0
Conversely, it interprets the following as False:
- The boolean
False - The strings
'false','no','off','0',''(empty string) - The integers
0and0.0
Strict Mode
When strict=True is set in the BoolSchema, no coercion is performed. Only actual Python bool values are accepted.
from pydantic_core import SchemaValidator, core_schema as cs
# Lax mode (default)
v_lax = SchemaValidator(cs.bool_schema())
assert v_lax.validate_python('yes') is True
assert v_lax.validate_python(1) is True
# Strict mode
v_strict = SchemaValidator(cs.bool_schema(strict=True))
# v_strict.validate_python('true') # Raises ValidationError
This behavior is tested extensively in pydantic-core/tests/validators/test_bool.py, which demonstrates that even in lax mode, values like 2 or 'cheese' will trigger a bool_parsing error because they cannot be unambiguously interpreted as truthy or falsy.
The Any Schema: Universal Validation and Serialization
The AnySchema serves as a catch-all. While its validation logic is a simple pass-through, its serialization logic is the most complex in the codebase.
Validation
During validation, AnySchema accepts any input without modification. It is often used as a default when a more specific schema cannot be determined or when arbitrary data structures are expected.
Serialization Complexity
Because AnySchema does not know the type of data it will encounter, its serializer (implemented in Rust as AnySerializer) must perform runtime type inspection. As shown in pydantic-core/tests/serializers/test_any.py, it can serialize:
- Standard Types:
int,str,list,dict. - Complex Types:
datetime,date,time,timedelta,UUID,Enum. - Pydantic Entities: Classes that define
__pydantic_serializer__(like Models and Dataclasses).
from datetime import datetime
from pydantic_core import SchemaSerializer, core_schema
s = SchemaSerializer(core_schema.any_schema())
# AnySchema handles complex types during serialization automatically
assert s.to_json(datetime(2022, 12, 3)) == b'"2022-12-03T00:00:00"'
assert s.to_python({'a': 1}) == {'a': 1}
Circular References and Fallbacks
A critical feature of AnySchema serialization is circular reference detection. If a list or dictionary contains a reference to itself, the serializer will raise a ValueError during JSON serialization to prevent infinite loops.
Additionally, AnySchema supports a fallback function. If the serializer encounters a type it doesn't recognize (and which doesn't have a Pydantic serializer attached), it can call this user-provided function instead of raising a PydanticSerializationError.
Design Tradeoffs
Performance vs. Flexibility
The AnySchema is extremely fast for validation because it does nothing. However, for serialization, it is generally slower than a specific schema (like IntSchema or StrSchema) because it must check the type of every object at runtime.
Strictness as a Toggle
The inclusion of the strict flag in BoolSchema and NullableSchema reflects a design philosophy of "Lax by default, Strict by choice." This allows the core to handle messy real-world data (like JSON strings representing booleans) while providing developers the tools to enforce type safety when needed.
The Wrapper Pattern
Using NullableSchema as a wrapper rather than a Union[T, None] is a performance optimization. It allows the underlying Rust implementation to check for null values at the very beginning of the validation loop, avoiding the overhead of iterating through union choices.