Skip to main content

Strict vs Lax Validation

Validation in this codebase is designed around a dual-mode philosophy: Lax and Strict. This design addresses the tension between developer convenience (allowing "123" to be treated as an integer) and type safety (ensuring an input is exactly the expected type).

The implementation relies on two primary mechanisms: global configuration via CoreConfig and fine-grained logic branching via LaxOrStrictSchema.

Global Strictness Control

The CoreConfig TypedDict in pydantic_core.core_schema serves as the central authority for validation behavior. The strict attribute within this config determines the default validation mode for an entire schema.

class CoreConfig(TypedDict, total=False):
# ...
strict: bool
# ...
coerce_numbers_to_str: bool # default: False

When strict is set to True, the validator generally bypasses type coercion. For example, a string input for an integer field will fail validation rather than being parsed. This global toggle allows developers to enforce strict type checking across complex models without configuring every individual field.

Logic Branching with LaxOrStrictSchema

While global configuration is powerful, certain types require different validation logic entirely depending on the mode. The LaxOrStrictSchema class enables this by defining two distinct validation paths.

class LaxOrStrictSchema(TypedDict, total=False):
type: Required[Literal['lax-or-strict']]
lax_schema: Required[CoreSchema]
strict_schema: Required[CoreSchema]
strict: bool
# ...

This structure allows the validator to switch between lax_schema and strict_schema at runtime. If the strict field is provided within the schema itself, it overrides the global CoreConfig setting for that specific branch.

Runtime Switching Example

The following example from tests/validators/test_lax_or_strict.py demonstrates how the validator chooses a path based on the strict argument provided during the validation call:

from pydantic_core import SchemaValidator, core_schema

v = SchemaValidator(core_schema.lax_or_strict_schema(
core_schema.str_schema(),
core_schema.int_schema()
))

# Default is lax: uses str_schema
assert v.validate_python('aaa') == 'aaa'

# Runtime strict=True: uses int_schema
assert v.validate_python(123, strict=True) == 123

In this scenario, the "lax" path expects a string, while the "strict" path expects an integer. This capability is essential for types like Path or Enum, where lax mode might allow a string that is then converted, while strict mode requires an instance of the actual class.

Type Coercion and Constraints

The distinction between modes is most visible in how type coercion is handled. In lax mode, the validator attempts to "coerce" inputs into the target type. A notable configuration option in CoreConfig is coerce_numbers_to_str.

When coerce_numbers_to_str is enabled, numeric types (like int or float) are converted to strings during validation. However, as defined in the CoreConfig documentation, this is "not applicable in strict mode." This highlights a core principle of the design: strict mode is a guarantee of type integrity, and configuration options that facilitate coercion are ignored to maintain that guarantee.

Design Tradeoffs

The dual-path approach in LaxOrStrictSchema introduces a tradeoff between flexibility and schema complexity.

  1. Performance: By separating the paths, the validator can execute highly optimized code for strict validation (which often involves simple type checks) without the overhead of coercion logic.
  2. Schema Size: Defining two schemas for a single field increases the size of the CoreSchema tree. This is why LaxOrStrictSchema is typically reserved for types where coercion is non-trivial, such as Path validation in pydantic/_internal/_generate_schema.py, where the lax schema might include a union of strings and instances, while the strict schema only allows instances.
  3. Consistency: The implementation ensures that even if a global strict setting is active, specific sub-schemas can opt-out or provide specialized behavior, allowing for a "strict by default, lax where necessary" architecture.