Skip to main content

Deep dive into pydantic BaseModel class decorators

· 9 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

Pydantic BaseModel class decorators are a powerful and modern way to customize a model's behavior and validation logic. While a lot of Pydantic's functionality is configured through class attributes or the ConfigDict, decorators offer a more explicit and code-centric approach, especially for complex validation.

Pydantic V2 introduced several new decorators to enhance validation and model configuration. The most important ones are:

  • @model_validator
  • @field_validator
  • @computed_field

These decorators are typically imported from pydantic.

@model_validator: Cross-Field Validation 🧠

The @model_validator decorator is used to validate multiple fields at once. This is essential when the validity of one field depends on the value of another. A simple field_validator can't handle this logic because it only has access to a single field's data.

The decorator can be used in two modes: before and after.

  • @model_validator(mode='before'): This runs before any other validation, and the validator function receives the raw input data (e.g., a dictionary from a JSON payload). This is useful for pre-processing data before Pydantic's built-in validation.

  • @model_validator(mode='after'): This runs after all field-level validation has passed. The validator function receives a ValidationInfo object and the validated data (which is a dictionary of the Pydantic model's fields and their values). This is the most common use case for cross-field validation.

Example 1: Validating a Start and End Date

Let's validate that an event's end_date does not come before its start_date.

from datetime import date
from pydantic import BaseModel, ValidationError, model_validator

class Event(BaseModel):
name: str
start_date: date
end_date: date

@model_validator(mode='after')
def check_dates(self) -> 'Event':
if self.start_date > self.end_date:
raise ValueError('End date cannot be before start date.')
return self

try:
# This will pass
Event(name='Conference', start_date='2025-10-20', end_date='2025-10-25')

# This will raise an error
Event(name='Workshop', start_date='2025-10-20', end_date='2025-10-18')
except ValidationError as e:
print(e)

In this example, the check_dates validator runs after start_date and end_date have been converted to date objects. It then performs a simple check and raises an error if the condition isn't met.

Example 2: Enforcing a Conditional Field

A common scenario is when a field's presence depends on another field's value.

from typing import Optional
from pydantic import BaseModel, model_validator

class Payment(BaseModel):
method: str
card_number: Optional[str] = None
expiry_date: Optional[str] = None

@model_validator(mode='after')
def check_card_details(self) -> 'Payment':
if self.method == 'credit_card' and not self.card_number:
raise ValueError('Card number is required for credit card payments.')
return self

try:
# This will pass
Payment(method='credit_card', card_number='1234...', expiry_date='12/26')

# This will raise an error
Payment(method='credit_card')
except ValueError as e:
print(e)

The validator checks if the payment method is credit_card and, if so, ensures that card_number is provided.


@field_validator: Single-Field Validation and Transformation ✨

The @field_validator decorator is used to validate or transform the data of a single field. It is the successor to the validator decorator from Pydantic V1. It is more explicit and powerful because you can choose its mode and target one or more fields.

  • @field_validator('my_field'): The decorator takes one or more field names as arguments.
  • mode='before': The validator receives the raw input value for the field.
  • mode='after': The validator receives the validated, type-coerced value for the field.

Example 3: Pre-processing a Username

Let's strip whitespace and convert a username to lowercase before it is validated.

from pydantic import BaseModel, field_validator

class User(BaseModel):
username: str

@field_validator('username', mode='before')
def sanitize_username(cls, v: str) -> str:
if not isinstance(v, str):
raise ValueError('Username must be a string')
return v.strip().lower()

user = User(username=' JaneDoe ')
print(user.username)
# Output: janedoe

By using mode='before', the sanitize_username function gets the raw input string ' JaneDoe ' and returns the cleaned version 'janedoe', which Pydantic then uses to validate the username field.

Example 4: Validating a Phone Number Format

This validator runs after the value has been coerced to a string and checks its format with a regular expression.

import re
from pydantic import BaseModel, field_validator

class Contact(BaseModel):
phone_number: str

@field_validator('phone_number')
def validate_phone(cls, v: str) -> str:
if not re.match(r'^\d{3}-\d{3}-\d{4}$', v):
raise ValueError('Phone number must be in XXX-XXX-XXXX format')
return v

try:
# This will pass
contact = Contact(phone_number='123-456-7890')
print(contact.phone_number)

# This will raise an error
Contact(phone_number='1234567890')
except ValueError as e:
print(e)

This is an example of an after mode validator (the default mode).


@computed_field: Creating a Dynamic Field 💻

The @computed_field decorator is used to define a field whose value is computed from other fields in the model. Computed fields are read-only and are not included in the input data when creating a model instance.

Example 5: Calculating a User's Full Name

Let's compute a full_name field from first_name and last_name.

from pydantic import BaseModel, computed_field

class User(BaseModel):
first_name: str
last_name: str

@computed_field
@property
def full_name(self) -> str:
return f'{self.first_name} {self.last_name}'

# Create a user instance
user = User(first_name='John', last_name='Doe')

# Access the computed field
print(user.full_name)
# Output: John Doe

The full_name field is automatically calculated when you access it. It's a great way to derive data without storing it explicitly in the database or passing it through the API [1, 2].


why and when to Use Decorators

  • When to use @field_validator:

    • To sanitize or format a single field's data (e.g., lowercasing an email).
    • To enforce a specific format or pattern on a field (e.g., a regex for a password).
    • To perform a sanity check on a single value (e.g., ensuring a number is positive).
  • When to use @model_validator:

    • When a field's validity depends on the value of another field.
    • To implement complex business logic that involves multiple attributes of the model.
    • For pre-processing raw input data before Pydantic's core validation.
  • When to use @computed_field:

    • To create read-only fields that are derived from other data in the model.
    • To add convenient, dynamic properties to your model without extra storage or API payload.

Sources

  1. "Pydantic V2: A Comprehensive Guide for Beginners and Experts." Codementor. https://www.codementor.io/@suyashverma/pydantic-v2-a-comprehensive-guide-for-beginners-and-experts-1456s14x7t
  2. "Pydantic Computed Fields." Pydantic Docs. https://docs.pydantic.dev/latest/usage/fields/#computed-fields
  3. "Pydantic: A Comprehensive Guide." Real Python. https://realpython.com/pydantic-guide/