Here is a comprehensive set of technical diagrams depicting how Pydantic works internally, with accompanying notes.
1. BaseModel
Creation Lifecycle
This diagram illustrates the process that occurs when a class inheriting from BaseModel
is defined. The ModelMetaclass
intercepts the class creation, inspects its definition, builds a pydantic-core
schema, and attaches the necessary validation and serialization machinery to the final class.
graph TD
subgraph Pydantic User Code
A["class MyModel(BaseModel):, title: str = 'Default', age: int"]
end
subgraph Pydantic Internals
B{{"ModelMetaclass.__new__"}}
C["Inspect class namespace, (attributes, annotations, ConfigDict, decorators)"]
D["_generate_schema.GenerateSchema"]
E["Process field annotations, (e.g., `int`, `Annotated[...]`)"]
F{"__get_pydantic_core_schema__ hook"}
G["Build CoreSchema"]
H["Create SchemaValidator, (from CoreSchema)"]
I["Create SchemaSerializer, (from CoreSchema)"]
J["Construct final model class"]
end
A -- Triggers --> B
B -- Uses --> C
C -- Instantiates --> D
D -- Calls --> E
E -- Can call --> F
F -- Returns CoreSchema part --> E
E -- Builds --> G["Build CoreSchema, (a dict defining validation/serialization logic)"]
G -- Used to create --> H
G -- Used to create --> I
H -- Attached to --> J
I -- Attached to --> J
C -- Defines body of --> J
J -- Returns --> K["MyModel Class, (with __pydantic_validator__, __pydantic_serializer__, etc.)"]
%% Styling
style A fill:#f9f,stroke:#333,stroke-width:2px,color:#000
style K fill:#ccf,stroke:#333,stroke-width:2px,color:#000
classDef internal fill:#e6ffed,stroke:#2E8B57,stroke-width:1px,color:#000
class B,C,D,E,F,G,H,I,J internal
Notes:
ModelMetaclass
: This is the powerhouse behind BaseModel
. When you define a class that inherits from BaseModel
, its __new__
method is executed.- Namespace Inspection: The metaclass scans the class definition for field annotations, default values,
ConfigDict
, and decorators like @field_validator
. GenerateSchema
: This internal class is responsible for converting Python type annotations into a pydantic-core
schema. It recursively processes types.__get_pydantic_core_schema__
: This is the primary hook for customizing schema generation. Pydantic calls this on types and annotations (like Annotated[int, Gt(0)]
) to build up the final CoreSchema
.CoreSchema
: This is a dictionary that declaratively defines all validation and serialization logic. It’s the blueprint passed to pydantic-core
.SchemaValidator
/SchemaSerializer
: These are the workhorses from the pydantic-core
Rust library. They are compiled from the CoreSchema
and perform the actual high-performance validation and serialization. They get attached to the model class as __pydantic_validator__
and __pydantic_serializer__
.
2. Core Validation Workflow
This diagram shows the sequence of events when you validate data, for example by instantiating a model or calling model_validate
. It highlights how pydantic-core
is the engine and how functional validators are integrated.
graph TD
subgraph AA["Pydantic User Code"]
A["MyModel.model_validate(data)"]
end
subgraph BB["Pydantic Internals (Python)"]
B["model.__pydantic_validator__.validate_python(data)"]
C{"Is Plugin System Active?"}
D["PluggableSchemaValidator"]
E["Call `on_enter` handlers"]
I["Call `on_success` or, `on_error` handlers"]
end
subgraph CC["Pydantic Core (Rust)"]
F["SchemaValidator.validate_python()"]
G["Process input against, CoreSchema"]
H{{"Validation Result"}}
end
A -- Calls --> B
B --> C
C -- Yes --> D
C -- No --> F
D -- Calls --> E
E -- Wraps call to --> F
F -- Traverses --> G
G -- Calls back to Python for --> G_V["\`before\`, \`after\`, \`wrap\`, \`plain\` validators"]
G -- Produces --> H
H -- Success --> I
H -- Failure (ValidationError) --> I
I -- Returns --> K["Validated Model / ValidationError"]
H -- Returns --> K
%% Styling
style A fill:#f9f,stroke:#333,stroke-width:2px,color:#000
style K fill:#ccf,stroke:#333,stroke-width:2px,color:#000
classDef internal fill:#e6ffed,stroke:#2E8B57,stroke-width:1px,color:#000
class B,C,D,E,I,G_V internal
classDef core fill:#e3f2fd,stroke:#0d47a1,stroke-width:1px,color:#000
classDef bfill fill:#b3a4a1
class F,G,H core
class AA,BB,CC bfill
Notes:
- Entry Point: Validation starts with methods like
model_validate()
or TypeAdapter.validate_python()
. __pydantic_validator__
: This attribute on the model holds the SchemaValidator
instance.PluggableSchemaValidator
: If any plugins are registered, this wrapper is used to invoke plugin event handlers (on_enter
, on_success
, on_error
) around the core validation logic.SchemaValidator
(pydantic-core): The high-performance validation engine written in Rust. It takes the input data and the CoreSchema
built during model creation.- Callback Validators: The
CoreSchema
can contain references to Python functions for custom logic (e.g., from @field_validator
). pydantic-core
calls back into the Python world to execute these functions at the appropriate step (before
, after
, etc.). - Result: The process returns a validated model instance on success or raises a
ValidationError
on failure.
3. Core Serialization Workflow
This diagram outlines how a model instance is converted into a dictionary, which is the basis for model_dump()
and model_dump_json()
.
graph TD
subgraph AA["Pydantic User Code"]
A["my_instance.model_dump()"]
end
subgraph BB["Pydantic Internals (Python)"]
B["model.__pydantic_serializer__.to_python(my_instance)"]
end
subgraph CC["Pydantic Core (Rust)"]
C["SchemaSerializer.to_python()"]
D["Process instance against, CoreSchema"]
E{{"Serialization Result"}}
end
A -- Calls --> B
B -- Calls --> C
C -- Traverses --> D
D -- Calls back to Python for --> D_S["\`model_serializer\`, \`field_serializer\`"]
D -- Produces --> E
E -- Returns --> F["Python Dictionary"]
%% Styling
style A fill:#f9f,stroke:#333,stroke-width:2px,color:#000
style F fill:#ccf,stroke:#333,stroke-width:2px,color:#000
classDef internal fill:#e6ffed,stroke:#2E8B57,stroke-width:1px,color:#000
class B,D_S internal
classDef core fill:#e3f2fd,stroke:#0d47a1,stroke-width:1px,color:#000
class C,D,E core
classDef bfill fill:#b3a4a1
class AA,BB,CC bfill
Notes:
- Entry Point: Serialization starts with
model_dump()
or model_dump_json()
. __pydantic_serializer__
: This attribute holds the SchemaSerializer
instance, created from the same CoreSchema
as the validator.SchemaSerializer
(pydantic-core): This Rust component traverses the model instance’s attributes.- Callback Serializers: If
@field_serializer
or @model_serializer
were used, the CoreSchema
contains references to these functions. pydantic-core
calls them to customize the output for specific fields or the entire model. - Result: The process returns a Python dictionary.
model_dump_json()
then serializes this dictionary to a JSON string.
4. Schema Generation Process
This diagram provides an overview of how Pydantic generates both its internal CoreSchema
and the public-facing JSON Schema.
graph TD
subgraph AA["Schema Generation Flow"]
A["Type Annotation, (e.g., \`int\`, \`BaseModel\`, \`Annotated\`)"] -- "Processed by" --> B["_generate_schema.GenerateSchema"]
B -- "Calls" --> C{"__get_pydantic_core_schema__ hook"}
C -- "Returns parts of" --> D["CoreSchema, (internal representation)"]
D -- "Used by" --> E["\`SchemaValidator\` &, \`SchemaSerializer\`"]
D -- "Input for" --> F["json_schema.GenerateJsonSchema"]
F -- "Traverses CoreSchema and calls" --> G["\`int_schema\`, \`model_schema\`, etc."]
G -- "Can be customized by" --> H{"__get_pydantic_json_schema__ hook"}
H -- "Returns parts of" --> I["JSON Schema, (public representation)"]
I -- "Returned by" --> J["\`MyModel.model_json_schema()\`"]
end
%% Styling
classDef internal fill:#e6ffed,stroke:#2E8B57,stroke-width:1px,color:#000
class B,C,D,F,G,H,I internal
classDef core fill:#e3f2fd,stroke:#0d47a1,stroke-width:1px,color:#000
class E core
classDef user fill:#fff0f0,stroke:#c0392b,stroke-width:1px,color:#000
class A,J user
style AA fill:#b3a4a1
Notes:
- Two Schemas: Pydantic uses two distinct schema representations. The
CoreSchema
is an internal, detailed blueprint for pydantic-core
. The JSON Schema is for external tools and documentation. - CoreSchema First: The
CoreSchema
is always generated first from the Python type annotations. This is the source of truth for validation and serialization. __get_pydantic_core_schema__
: This is the hook for types to define how they should be validated. Pydantic uses this to build the CoreSchema
.- JSON Schema Second: The
GenerateJsonSchema
class takes a completed CoreSchema
as input and translates it into a standard JSON Schema dictionary. __get_pydantic_json_schema__
: This hook allows for customization of the final JSON Schema output, acting as an override layer on top of the default translation logic.
5. TypeAdapter
Workflow
The TypeAdapter
is a tool for applying Pydantic’s validation and serialization to arbitrary types, not just BaseModel
subclasses. This diagram shows its initialization and usage.
graph TD
subgraph AA["Pydantic User Code"]
A["ta = TypeAdapter(list[int])"]
E["ta.validate_python(['1', 2])"]
G["ta.dump_python([1, 2])"]
end
subgraph BB["TypeAdapter Internals"]
B["\`__init__\` discovers parent frame, for ForwardRef resolution"]
C["Instantiates \`_generate_schema.GenerateSchema\`"]
D["Builds \`CoreSchema\` for \`list[int]\`"]
DA["Creates \`SchemaValidator\`"]
DB["Creates \`SchemaSerializer\`"]
F["Delegates to \`self.validator\`"]
H["Delegates to \`self.serializer\`"]
end
A -- Triggers --> B
B --> C
C --> D
D --> DA
D --> DB
subgraph CC["Result"]
I["[1, 2]"]
J["[1, 2]"]
end
E -- Calls --> F
F -- Returns --> I
G -- Calls --> H
H -- Returns --> J
%% Styling
style A,E,G fill:#f9f,stroke:#333,stroke-width:2px,color:#000
style I,J fill:#ccf,stroke:#333,stroke-width:2px,color:#000
classDef internal fill:#e6ffed,stroke:#2E8B57,stroke-width:1px,color:#000
class B,C,D,DA,DB,F,H internal
style AA fill:#b3a4a1
style BB fill:#b3a4a1
style CC fill:#b3a4a1
Notes:
- Purpose:
TypeAdapter
acts as a lightweight, single-type version of a BaseModel
. - Initialization: When created, it performs the same schema generation process as a
BaseModel
but for a single, specified type. It determines its context to resolve forward references. - Delegation: Its
validate_python
and dump_python
methods are thin wrappers that delegate directly to its internal SchemaValidator
and SchemaSerializer
instances.
6. @validate_call
Decorator
This diagram explains how the @validate_call
decorator works to add runtime type validation to function calls.
graph TD
subgraph AA["Pydantic User Code"]
A["@validate_call, def my_func(a: int, b: str): ..."]
D["decorated_func(a='1', b='hello')"]
end
subgraph BB["Decorator Internals"]
B["\`validate_call\` returns a wrapped function"]
C["Wrapper inspects \`my_func\` signature"]
C1["Builds \`CoreSchema\` for arguments"]
C2["Creates \`SchemaValidator\` for arguments"]
E["Wrapper intercepts the call"]
F["Uses validator on \`*args\`, \`**kwargs\`"]
G["Calls original \`my_func\` with, validated/coerced arguments"]
H{"(Optional) Validates, return value"}
end
subgraph CC["Result"]
I["Returns result from \`my_func\`"]
end
A -- Creates --> B
B -- Contains --> C
C --> C1 --> C2
D -- Triggers --> E
E -- Uses validator --> F
F -- On success --> G
G -- Calls original --> H
H -- Returns --> I
%% Styling
style A,D fill:#f9f,stroke:#333,stroke-width:2px,color:#000
style I fill:#ccf,stroke:#333,stroke-width:2px,color:#000
classDef internal fill:#e6ffed,stroke:#2E8B57,stroke-width:1px,color:#000
class B,C,C1,C2,E,F,G,H internal
style AA fill:#b3a4a1
style BB fill:#b3a4a1
style CC fill:#b3a4a1
Notes:
- Decoration Time: When the function is decorated,
@validate_call
creates a wrapper. This wrapper inspects the function’s signature and builds a CoreSchema
specifically for its arguments. A SchemaValidator
is then created from this schema. - Call Time: When the decorated function is called, the wrapper intercepts the arguments.
- Validation: It uses its internal
SchemaValidator
to validate and coerce the arguments. This is effectively like using a TypeAdapter
on the function’s signature. - Execution: If validation succeeds, the original function is called with the now-validated arguments.
- Return Validation: If configured, the return value is also validated.
7. Plugin System
This diagram shows how third-party plugins can hook into Pydantic’s validation lifecycle.
graph TD
subgraph AA["Plugin Loading"]
A["Pydantic initializes"] -- "Calls" --> B["\`plugin._loader.get_plugins()\`"]
B -- "Uses \`importlib.metadata\` to find" --> C["Entry points in \`pydantic\` group"]
C -- "Loads" --> D["Plugin objects, (implementing \`PydanticPluginProtocol\`)"]
end
subgraph BB["Validation with Plugins"]
E["\`BaseModel\` or \`TypeAdapter\` created"] -- "Needs a validator" --> F{"Plugins are loaded?"}
F -- Yes --> G["Create \`PluggableSchemaValidator\`"]
G -- "Calls \`new_schema_validator\` on each" --> D
D -- "Returns" --> H["Event Handlers, (\`on_enter\`, \`on_success\`, etc.)"]
G -- "Wraps \`SchemaValidator\` methods" --> I["Wrapped \`validate_python\`"]
I -- "Calls" --> J["1 \`on_enter\` handlers"]
J -- "Calls" --> K["2 Original \`SchemaValidator.validate_python\`"]
K -- "Calls" --> L["3 \`on_success\` or, \`on_error\` handlers"]
end
F -- No --> M["Create standard `SchemaValidator`"]
%% Styling
classDef internal fill:#e6ffed,stroke:#2E8B57,stroke-width:1px,color:#000
class A,B,C,E,F,G,H,I,J,K,L,M internal
classDef plugin fill:#fffbe6,stroke:#fbc02d,stroke-width:1px,color:#000
class D plugin
style AA fill:#b3a4a1
style BB fill:#b3a4a1
Notes:
- Discovery: Plugins are discovered at runtime using Python’s standard
entry_points
mechanism. Pydantic looks for plugins registered under the pydantic
group. PydanticPluginProtocol
: A valid plugin is an object that implements the new_schema_validator
method.PluggableSchemaValidator
: When plugins are detected, this wrapper class is used instead of the raw SchemaValidator
.- Hooks:
PluggableSchemaValidator
calls the new_schema_validator
method on each loaded plugin. The plugin can return a set of handler methods for different validation events (on_enter
, on_success
, on_error
, on_exception
). - Execution Flow: The
PluggableSchemaValidator
’s validation methods are wrappers that execute the registered plugin handlers before and after calling the actual pydantic-core
validation logic. This allows plugins to monitor, instrument, or even alter the validation process.