Back to Blog
Python

Python Pydantic field_validator and model_validator

python pydantic field_validator and model_validator: Learn how to use field_validator and model_validator in Pydantic for precise data validation, including syntax, di...

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

Understanding python pydantic field_validator and model_validator is key to implementing precise validation in Pydantic models. These two decorators control validation scope: field_validator for individual fields and model_validator for the entire model. Both are part of the Pydantic library and are used to attach custom validation logic to a model. The choice between them depends on the scope of the check you need to perform.

The Problem field_validator and model_validator Solve

When building data models with Pydantic, validation often needs to happen at two levels: individual fields and the model as a whole. A field may need a constraint that depends only on its own value, such as ensuring an integer is positive. But other constraints require seeing multiple fields together, such as checking that a start date precedes an end date. Pydantic provides two decorators for these cases: field_validator and model_validator. Both are part of the pydantic library and are used to attach custom validation logic to a model. The choice between them depends on the scope of the check you need to perform.

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, like 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. The method must be a class method, so @classmethod is required. The value parameter receives the raw input for that field. The method returns the validated value, which can be transformed if needed. If validation fails, raise a ValueError or AssertionError. Pydantic will wrap it into a ValidationError when the model is instantiated.

field_validator can also be applied to multiple fields by passing multiple names, but each call validates one field at a time. 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 entire model after all fields have been individually validated. It receives the model instance (or a dictionary of values depending on mode) 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. You can access attributes and return the instance. There is also mode="before" which receives a dictionary of raw input values before field validation. The after mode is more common because it allows you to work with already-validated fields.

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

field_validator is also faster because it doesn't need to build the full model context. But for complex models, the overhead of model_validator is negligible compared to the validation logic itself.

Execution Order and Interaction Between Validators

Understanding order matters when validators depend on each other. Pydantic runs field validators for each field 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 feeds back into another field, you must 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 the total after both fields 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 matches a user level. The date range requires cross-field validation, while the discount code is a single-field check.

from pydantic import BaseModel, field_validator, model_validator from datetime import date class Booking(BaseModel): check_in: date check_out: date user_level: str 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 catches it and adds it to the model's ValidationError. The error includes the location: for a field_validator, the location is the field name; for a model_validator, it's the model itself (often shown as __root__ or the model name). This affects how you handle errors in your application.

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

The error output will show two issues: one for the discount code and one for the date order. This granularity helps clients understand what went wrong.

Performance and Maintainability Considerations

field_validator is slightly more efficient because it avoids building the full model context for each field. However, the difference is rarely the bottleneck. The real cost is the validation logic itself. For high-throughput APIs, you might want to profile whether your validators are doing unnecessary work, such as regex compilation inside the validator. You can precompile patterns outside the class to avoid repeated overhead.

Maintainability benefits from using the right validator for the right scope. Putting cross-field logic in a field_validator often leads to hacks like storing temporary state on the class, which is error-prone. Using model_validator keeps the intent clear. Also, both decorators support mode="before" for raw input validation, which can be useful for sanitizing data before type coercion, but it adds complexity.

Common Pitfalls and Edge Cases

One pitfall is forgetting the @classmethod decorator. Pydantic 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, you'll 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's safer to modify the existing instance and return it.

Also, field_validator can be applied to a field that is a Optional type. The validator receives None if the field is not provided. You need to 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.

python pydantic field_validator and model_validator: Practic | RYUSLOG DEV