← Back to 20250617

120 pydantic diags

Documentation for 120_pydantic_diags from the Ds ex repository.

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.