Core Schema
The heart of Pydantic v2 is that `BaseModel` does not directly perform validation. Instead, Pydantic turns annotations and constraints into an intermediate representation called core schema, and `pydantic-core` executes that schema. Once you see that structure, Pydantic's performance model and extension model become much clearer.
Quick takeaway: core schema is the compiled graph of your Python type hints, constraints, and hooks. `BaseModel`, `Annotated`, field constraints, validators, and `TypeAdapter` are all ways of producing or consuming that schema.
Start With the Flow
Why It Matters
- It explains why
TypeAdaptercan validate arbitrary types without a model class. - It explains how validators, field constraints, and
Annotatedmetadata combine. - It gives you the right mental model for custom types and framework integrations.
A Minimal Code Example
from typing import Annotated
from pydantic import AfterValidator, BaseModel, TypeAdapter
def positive(value: int) -> int:
if value <= 0:
raise ValueError("positive only")
return value
PositiveInt = Annotated[int, AfterValidator(positive)]
class LineItem(BaseModel):
qty: PositiveInt
adapter = TypeAdapter(list[PositiveInt])
print(LineItem.model_validate({"qty": "3"}))
print(adapter.validate_python(["1", 2, 3]))
print("adapter core schema type:", adapter.core_schema["type"])`LineItem` and `TypeAdapter(list[PositiveInt])` look like different APIs, but both are built on top of core schema and the same pydantic-core validator machinery.
What Usually Ends Up in Core Schema
- primitive and container type information
- structure for models, unions, and nested types
- constraints such as length, bounds, regex, and strictness
- hooks from validators and serializers
- metadata carried by
Annotated
Why Annotated Matters
In Pydantic v2, many useful runtime validation hooks are expressed through Annotated, so typing metadata and runtime validation metadata meet in the same declaration site.
from typing import Annotated
from pydantic import Field, TypeAdapter
UserId = Annotated[int, Field(gt=0, description="database primary key")]
adapter = TypeAdapter(UserId)
print(adapter.validate_python("10"))When You Need Core-Schema Intuition
Custom reusable types
You want validation rules to live at the type level rather than inside one model field.
TypeAdapter reuse
You want fast validation for arbitrary typed structures without wrapping them in model classes.
Framework integration
You are reading annotations and turning them into schemas or validators.
Performance reasoning
You want to understand that Pydantic is not running ad hoc Python checks from scratch for every input.