Back to Blog
Python

Pydantic field_validator and model_validator in Python

Learn how to use Pydantic's field_validator and model_validator for single-field and cross-field validation, including syntax and practical examples.

pydanticvalidationpythondata validationfield_validatormodel_validator
Diagram showing a Pydantic model with field_validator applied to individual fields and model_validator applied to the whole model

In Pydantic v2, field_validator and model_validator add custom validation to a model at two different scopes. field_validator validates or transforms one field, while model_validator runs after all fields have been validated and can inspect the model as a whole. Use field_validator for rules that depend only on one field's value, and model_validator for checks that compare multiple fields.

Using field_validator for Single-Field Validation

field_validator is applied to a method that validates a single field. The method receives the value of that field and returns the validated value. You can use it to enforce constraints that are local to one field, such as normalizing strings or checking numeric ranges.

from pydantic import BaseModel, field_validator class User(BaseModel): name: str age: int @field_validator("name") @classmethod def name_must_not_be_empty(cls, value: str) -> str: if not value.strip(): raise ValueError("name cannot be empty") return value.strip()

The decorator takes the field name as an argument. In Pydantic v2, validators are class methods, so @classmethod is required. The value parameter receives the field's value after Pydantic has parsed and coerced it to the declared type. The method returns the validated value, which can be transformed if needed. If validation fails, raise a ValueError or AssertionError; Pydantic wraps it in a ValidationError when the model is instantiated.

field_validator can also validate multiple fields by passing multiple names, but each call still validates one field at a time. In the default after mode, it runs after the field's type validation, so the value has already been coerced to the declared type.

Using model_validator for Cross-Field Validation

model_validator operates on the model after all fields have been validated. In mode="after", it receives the model instance and can inspect or modify multiple fields. This is the right place for checks that involve relationships between fields.

from pydantic import BaseModel, model_validator class Event(BaseModel): start: int end: int @model_validator(mode="after") @classmethod def check_order(cls, values): if values.start >= values.end: raise ValueError("start must be before end") return values

In mode="after", the validator receives the model instance, so you can access attributes with dot notation and return the instance. mode="before" receives the raw input dictionary before field validation; it is useful for sanitizing data before type coercion, but is less common because the fields are not yet validated.

How field_validator and model_validator Differ

The core difference is scope. field_validator is for one field at a time, while model_validator sees the whole model. This affects when they run, what they can access, and how they modify data.

Aspectfield_validatormodel_validator
ScopeSingle fieldEntire model
AccessOnly the field's valueAll fields / model attributes
ModificationCan return a new value for that fieldCan modify model attributes or return new values
ExecutionAfter type validation of that fieldAfter all field validators have run
Typical useFormatting, range checks, regexCross-field dependencies, consistency checks

Choosing the narrower validator when possible keeps the validation rule close to the field it concerns. Cross-field logic belongs in model_validator; putting it in a field_validator usually requires storing temporary state, which is error-prone.

Execution Order and Interaction Between Validators

Understanding order matters when validators depend on each other. Pydantic runs field validators in the order the fields are declared. After all fields have been validated, model validators run. If you have multiple model_validator methods, they run in the order they are defined.

A common mistake is assuming that a model_validator can modify a field and have that change reflected in another field_validator. That is not possible because field validators have already run. If you need a cross-field transformation that uses another field, do it in the model validator and return the modified model.

from pydantic import BaseModel, field_validator, model_validator class Order(BaseModel): quantity: int price: float total: float = 0.0 @field_validator("quantity") @classmethod def quantity_positive(cls, value): if value <= 0: raise ValueError("quantity must be positive") return value @model_validator(mode="after") @classmethod def compute_total(cls, values): values.total = values.quantity * values.price return values

Here the field validator ensures quantity is positive. The model validator computes total after both quantity and price are available. The order is guaranteed: field validators first, then model validators.

Practical Example: Combining Both Validators

A realistic scenario is a booking system where you need to validate a date range and also ensure a discount code has the required prefix. The date range requires cross-field validation, while the discount code is a single-field check.

from pydantic import BaseModel, ValidationError, field_validator, model_validator from datetime import date class Booking(BaseModel): check_in: date check_out: date discount_code: str @field_validator("discount_code") @classmethod def validate_discount(cls, value): if not value.startswith("DISC"): raise ValueError("invalid discount code") return value.upper() @model_validator(mode="after") @classmethod def check_dates(cls, values): if values.check_out <= values.check_in: raise ValueError("check_out must be after check_in") return values

The field validator normalizes the discount code and ensures its format. The model validator verifies the date order. This separation keeps each validation focused and easier to test.

Error Handling and Validation Context

When a validator raises an exception, Pydantic collects it in a ValidationError. Each error entry includes a location: for a field_validator, the location is the field name; for a model_validator, the error is reported at the model level rather than at an individual field. You can use e.errors() to inspect the failures.

try: Booking( check_in="2024-01-10", check_out="2024-01-05", discount_code="disc123", ) except ValidationError as e: print(e.errors())

This example reports both the invalid discount code and the reversed date order, which helps clients understand what went wrong.

Performance and Maintainability Considerations

Avoid putting unnecessary work inside validators. For example, compile a regular expression once outside the class and reuse it inside the validator. Prefer the validator with the smallest scope that can express the rule: use field_validator for a field-local rule and model_validator when multiple fields are involved. Both decorators also support mode="before" for raw input validation, but that mode adds complexity because the input has not yet been coerced.

Common Pitfalls and Edge Cases

One pitfall is forgetting the @classmethod decorator. Pydantic v2 requires validators to be class methods; omitting it raises a TypeError at class definition time. Another is using mode="before" without understanding that the value is a dictionary, not the model instance. If you try to access attributes before field validation, you will get an error.

A subtle edge case is when a model_validator returns a new model instance instead of modifying the existing one. That is allowed, but it can lead to unexpected behavior if you have multiple validators. It is safer to modify the existing instance and return it.

Also, if a field is optional, a field_validator receives None when the field is not provided. Handle that case explicitly.

@field_validator("nickname") @classmethod def validate_nickname(cls, value): if value is None: return value return value.strip()

This prevents a NoneType error when the field is optional.

Pydantic field_validator and model_validator: When to Use Each | RYUSLOG DEV