Skip to main content

Structured Objects

Pydantic provides several ways to define and validate structured data, ranging from full-featured classes to lightweight dictionary-like structures. The core of this functionality is built upon the pydantic-core Rust engine, which handles the heavy lifting of validation and serialization.

Base Models

The BaseModel class in pydantic/main.py is the primary interface for creating structured objects. When you inherit from BaseModel, Pydantic uses a metaclass to process your type annotations and build a validation schema.

Core Functionality

A BaseModel provides automatic validation on initialization and several methods for data manipulation:

  • __init__: Validates input data against the model's fields.
  • model_dump(): Converts the model instance into a Python dictionary.
  • model_dump_json(): Serializes the model instance to a JSON string.
  • model_construct(): A class method to create a model without validation (useful for trusted data).
from pydantic import BaseModel

class User(BaseModel):
id: int
name: str = 'Jane Doe'

# Validation happens on initialization
user = User(id='123')
assert user.id == 123
assert user.name == 'Jane Doe'

# Serialization
print(user.model_dump())
#> {'id': 123, 'name': 'Jane Doe'}

Internal Metadata

Models store their structural information in several "dunder" attributes managed by Pydantic:

  • __pydantic_fields__: A dictionary mapping field names to FieldInfo objects.
  • __pydantic_validator__: The pydantic-core validator instance.
  • __pydantic_serializer__: The pydantic-core serializer instance.

Pydantic Dataclasses

For developers who prefer the standard library's dataclass pattern, Pydantic provides an enhanced version in pydantic/dataclasses.py. The @pydantic.dataclasses.dataclass decorator is a drop-in replacement for dataclasses.dataclass that adds Pydantic's validation engine.

Unlike BaseModel, Pydantic dataclasses do not inherit from a base class. Instead, the decorator wraps the class and injects validation logic into the __init__ method.

import pydantic.dataclasses

@pydantic.dataclasses.dataclass
class Item:
name: str
price: float

item = Item(name='Widget', price='10.50')
assert item.price == 10.5

If you apply @pydantic.dataclasses.dataclass to an existing standard library dataclass, Pydantic will create a subclass to add validation without modifying the original class.

TypedDicts and TypeAdapter

When working with structures that are not classes—such as TypedDict or primitive types—Pydantic uses the TypeAdapter class found in pydantic/type_adapter.py.

TypeAdapter provides an interface similar to BaseModel for types that don't support methods. This is particularly useful for validating TypedDict structures, which are treated as plain dictionaries at runtime.

from typing import Annotated
from typing_extensions import TypedDict
from pydantic import TypeAdapter, Field

class UserDict(TypedDict):
id: int
username: Annotated[str, Field(min_length=3)]

ta = TypeAdapter(UserDict)
validated = ta.validate_python({'id': '1', 'username': 'jdoe'})
assert validated == {'id': 1, 'username': 'jdoe'}

Note: On Python versions earlier than 3.12, you should use typing_extensions.TypedDict instead of the standard library version to ensure full compatibility with Pydantic's type resolution.

Configuration with ConfigDict

All structured objects in Pydantic can be configured using ConfigDict (defined in pydantic/config.py). This allows you to control behaviors like:

  • extra: Whether to 'ignore', 'allow', or 'forbid' extra fields.
  • frozen: Whether the object should be immutable.
  • strict: Whether to use strict type checking instead of coercion.
  • validate_assignment: Whether to re-validate data when a field is changed.

Applying Configuration

The method for applying configuration depends on the object type:

  • BaseModel: Set the model_config class attribute.
  • Dataclass: Pass the config argument to the decorator.
  • TypeAdapter: Pass the config argument to the constructor.
from pydantic import BaseModel, ConfigDict

class StrictModel(BaseModel):
model_config = ConfigDict(strict=True, extra='forbid')

value: int

# This would raise a ValidationError because '1' is a string and strict=True
# model = StrictModel(value='1')

Comparison of Structured Objects

FeatureBaseModelpydantic.dataclassesTypeAdapter (TypedDict)
InheritanceRequired (BaseModel)NoneNone
ValidationOn __init__On __init__Via validate_python
Methodsmodel_dump, etc.None (use RootModel or TypeAdapter)validate_python, dump_python
Runtime TypeClass instanceClass instancedict
Configmodel_config attrconfig parameterconfig parameter

While BaseModel is the most common choice for application logic, TypeAdapter with TypedDict is often preferred for API responses or legacy data structures where maintaining a plain dictionary is required.