Validations in Pydantic V2

Author:Murphy  |  View: 25727  |  Time: 2025-03-22 20:46:55

Pydantic is the Data validation library for Python, integrating seamlessly with FastAPI, classes, data classes, and functions. Data validation refers to the validation of input fields to be the appropriate data types (and performing data conversions automatically in non-strict modes), to impose simple numeric or character limits for input fields, or even impose custom and complex constraints.

With larger classes and more fields to perform validation on, and with validations being able to process and modify the raw inputs, it is important to know the different types of validators, and their order of precedence in execution.

This article will discuss the different types of validation that Pydantic offers and the order of precedence of the different types of validation with code examples, which are not covered in great detail in Pydantic's documentation. The focus will be on the validation of classes, also referred to as BaseModel.

Table of Contents


Types of Validation

Before, After, Plain, and Wrap modes of validation

Pydantic performs internal parsing and validation of parameters, such as performing data conversions. User-defined validations can be run before or after Pydantic's validation.

Before Validators

  • Using BeforeValidator or mode="before"
  • Runs before Pydantic's internal parsing and validation
  • Flexible: can modify raw input
  • Less type-safe: Have to deal with raw input

After Validators

  • Using AfterValidator or mode="after"
  • Runs after Pydantic's internal parsing and validation
  • More type-safe: easier to implement

Plain Validators

  • Using PlainValidator or mode="plain"
  • Similar to before validator but terminates validation immediately; no validators (not even Pydantic's validations) can run after

Wrap Validators

  • Using WrapValidator or mode="wrap"
  • Able to run code before or after Pydantic and other validators, or even terminate validation immediately
  • Flexible: Most flexible validator

Validators can be stacked and run in sequence, making the order of precedence important. In the next sections, the order of precedence of before, after, wrap and plain validators will be demonstrated.


Validating with Field function

Perform validation of data types and add metadata

Fields are part of Pydantic internal parsing and validation of parameters. It performs simple and basic validations, and can also add metadata to the fields of models.

Some (non-exhaustive) validations include:

  • Numeric: gt, lt, ge, le for greater than, less than, greater than or equals to or less than or equals to constraints
  • String: min_length, max_length, pattern for the length of string and regex pattern constraints
  • Decimal: max_digits, decimal_places for decimal constraints

Code Demonstration

import pytest
from pydantic import BaseModel, ValidationError
from pydantic.fields import Field
from typing_extensions import Annotated

class SampleFieldClass(BaseModel):
    number: Annotated[int, Field(description="positive number", ge=0)]

# Initialising class
SampleFieldClass(number=0)

with pytest.raises(ValidationError):
    SampleFieldClass(number=-1)

Validating with Annotated type

Perform validation of data types and add more validators

In the above code demonstration, we have seen how the Field class can be used together with Annotated. Annotated can accept more arguments – specifically the Before, After, Wrap, and Plain validators discussed in the first section.

When to use: To bind validation to a data type (as opposed to a Field or Model)

Order of Precedence

When ordering the validators, the code reads the validations in a top-to-bottom fashion but inserts before and plain validators at the top, after validators at the bottom, and wrap validators as a sandwich.

For example, defining before validator 1 -> before validator 2 inserts the second before-validator above and runs in the order of 2 -> 1. While defining after validator 1 -> after validator 2 inserts the second after-validator below and runs in the order of 1 -> 2.

Code Demonstration

In the code demonstration below, we define the before, after, wrap, and plain validators. We then use them in different order i.e., before -> after -> wrap, before -> wrap -> after, and so on to see the order of precedence.

from typing import Any

from pydantic import BaseModel, ValidationInfo, ValidatorFunctionWrapHandler
from pydantic.functional_validators import (
    AfterValidator,
    BeforeValidator,
    PlainValidator,
    WrapValidator,
)
from typing_extensions import Annotated

# Validators
def wrap_validator(
    v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
) -> Any:
    print(f"Before wrap validation with info {info}")
    handler(v)
    print(f"After wrap validation with info {info}")
    return v

def before_validator1(v: Any, info: ValidationInfo):
    print(f"Run before-validator1 with info {info}")
    return v

def before_validator2(v: Any, info: ValidationInfo):
    print(f"Run before-validator2 with info {info}")
    return v

def after_validator1(v: Any, info: ValidationInfo):
    print(f"Run after-validator1 with info {info}")
    return v

def after_validator2(v: Any, info: ValidationInfo):
    print(f"Run after-validator2 with info {info}")
    return v

def plain_validator(v: Any, info: ValidationInfo):
    print(f"Run plain-validator with info {info}")
    return v

# Using validators in class (note the order of precedence!)
class SampleModelBeforeAfter(BaseModel):
    number: Annotated[
        int,
        BeforeValidator(before_validator1),
        BeforeValidator(before_validator2),
        AfterValidator(after_validator1),
        AfterValidator(after_validator2),
    ]

class SampleModelAfterBefore(BaseModel):
    number: Annotated[
        int,
        AfterValidator(after_validator1),
        AfterValidator(after_validator2),
        BeforeValidator(before_validator1),
        BeforeValidator(before_validator2),
    ]

class SampleModelWrapBeforeAfter(BaseModel):
    number: Annotated[
        int,
        WrapValidator(wrap_validator),
        BeforeValidator(before_validator1),
        AfterValidator(after_validator1),
    ]

class SampleModelBeforeWrapAfter(BaseModel):
    number: Annotated[
        int,
        BeforeValidator(before_validator1),
        WrapValidator(wrap_validator),
        AfterValidator(after_validator1),
    ]

class SampleModelBeforeAfterWrap(BaseModel):
    number: Annotated[
        int,
        BeforeValidator(before_validator1),
        AfterValidator(after_validator1),
        WrapValidator(wrap_validator),
    ]

class SampleModelBeforeAfterPlainWrap(BaseModel):
    number: Annotated[
        int,
        BeforeValidator(before_validator1),
        AfterValidator(after_validator1),
        PlainValidator(plain_validator),
        WrapValidator(wrap_validator),
    ]

# Initialising class
SampleModelBeforeAfter(number=5)  # Before2 -> Before1 -> After1 -> After2
SampleModelAfterBefore(number=5)  # Before2 -> Before1 -> After1 -> After2
SampleModelWrapBeforeAfter(number=5)  # Before1 -> Wrap (before) -> Wrap (after) -> After
SampleModelBeforeWrapAfter(number=5)  # Wrap (before) -> Before -> Wrap (after) -> After
SampleModelBeforeAfterWrap(number=5)  # Wrap (before) -> Before -> After -> Wrap (after)
SampleModelBeforeAfterPlainWrap(number=5)  # Wrap (before) -> Plain -> Wrap (after)

Validating with field validator

Perform validation of fields and able to reuse validators

Field validator is similar to Annotated, in the sense that it validates for only one field of the model. This requires defining a function and specifying which field name to validate for. Defining function allows for code reuse if needed.

When to use: To bind validation to a Field

How to use: Implemented as a class method

Order of Precedence

Within one field, Pydantic's internal validation is executed before field validator decorator methods, unless mode="before" is specified. Similar to the order of precedence of Annotated, the class methods are read in a top-to-bottom manner with mode="before" class methods inserted at the top of the call stack, and mode="after" methods inserted at the bottom of the call stack. By default, mode="after" if not specified.

Between different fields, model fields are validated in the order they are defined.

Code Demonstration

In the code demonstration below, we define the before and after validators. We also have multiple fields to validate to see the order of precedence between different fields and within the same field.

import pytest
from pydantic import BaseModel, ValidationError
from pydantic.fields import Field
from pydantic.functional_validators import field_validator
from typing_extensions import Annotated

class SampleModelFieldValidator(BaseModel):
    number: Annotated[int, Field(ge=-1)]
    id: str

    @field_validator("id")  # mode="after" by default
    @classmethod
    def validate_id_after(cls, v: str):
        print("validate_id_after")
        return v

    @field_validator("number", mode="before")
    @classmethod
    def validate_number_before1(cls, v: int):
        print("validate_number_before1")
        return v

    @field_validator("number", mode="before")
    @classmethod
    def validate_number_before2(cls, v: int):
        print("validate_number_before2")
        return v

    @field_validator("number")  # mode="after" by default
    @classmethod
    def validate_number_after1(cls, v: int):
        print("validate_number_after1")
        if v < 0:
            raise ValueError("Number should be positive")
        return v

    @field_validator("number", mode="after")
    @classmethod
    def validate_number_after2(cls, v: int):
        print("validate_number_after2")
        return v

SampleModelFieldValidator(number=5, id="abc")
"""
validate_number_before2
validate_number_before1
(internal validation for number happens here)
validate_number_after1
validate_number_after2
(internal validation for id happens here)
validate_id_after
"""

with pytest.raises(ValidationError):
    SampleModelFieldValidator(number=-2, id="abc")
    # Stops at internal validation
    # Input should be greater than or equal to -1

Validating with model validator

Perform validation of multiple fields

Unlike field validator which only validates for only one field of the model, model validator can validate the entire model data (all fields!).

When to use: To bind validation to multiple Fields

How to use: In mode="before", it is implemented as a class method with data being passed in as a dictionary that maps all fields to their raw values. In mode="after", it is implemented as an instance method where all the fields are already being mapped to self variables.

Order of Precedence

The model validator acts as a sandwich for all the field validators, with mode="before" executing before all field validators and mode="after" executing after all field validators.

Code Demonstration

In the code demonstration below, we define field validators and model validators to see the order of precedence.

from typing import Any

from pydantic import BaseModel
from pydantic.functional_validators import field_validator, model_validator
from typing_extensions import Self

class SampleModelModelValidator(BaseModel):
    number: int

    @field_validator("number", mode="before")
    @classmethod
    def validate_number_before(cls, v: int):
        print("validate_number_before")
        return v

    @field_validator("number", mode="after")
    @classmethod
    def validate_number_after(cls, v: int):
        print("validate_number_after")
        return v

    @model_validator(mode="before")
    @classmethod
    def validate_model_before(cls, data: Any) -> Any:
        print("validate_model_before", data)
        return data

    @model_validator(mode="after")
    def validate_model_after(self) -> Self:
        print("validate_model_after", {"number": self.number})
        return self

SampleModelModelValidator(number=5)
"""
validate_model_before {'number': 5}
validate_number_before
validate_number_after
validate_model_after {'number': 5}
"""

Bonus: Use with computed_field

Validation order of precedence with computed fields

Computed fields are used to create new derived fields without having to explicitly define them. Computed fields have nothing to do with validation, and it is not possible to perform validations using field validators and model validators on the computed fields.

Order of Precedence

The fields are computed after all field validations and model validations are completed.

Code Demonstration

In the code demonstration below, we define computed field, field validators, and model validators to see the order of precedence.

from typing import Any

from pydantic import BaseModel
from pydantic.fields import computed_field
from pydantic.functional_validators import field_validator, model_validator
from typing_extensions import Self

class SampleModelComputedField(BaseModel):
    number: int
    id: str

    @field_validator("id", mode="after")
    @classmethod
    def validate_id_after(cls, v: int):
        print("validate_id_after")
        return v

    @field_validator("number", mode="before")
    @classmethod
    def validate_number_before(cls, v: int):
        print("validate_number_before")
        return v

    @field_validator("number", mode="after")
    @classmethod
    def validate_number_after(cls, v: int):
        print("validate_number_after")
        return v

    @model_validator(mode="before")
    @classmethod
    def validate_model_before(cls, data: Any) -> Any:
        print("validate_model_before", data)
        return data

    @model_validator(mode="after")
    def validate_model_after(self) -> Self:
        print("validate_model_after", {"number": self.number, "id": self.id})
        return self

    @computed_field
    def uuid(self) -> str:
        print("Create uuid")
        return f"{self.id}{self.number}"

_ = SampleModelComputedField(number=5, id="abc")
"""
validate_model_before {'number': 5, 'id': 'abc'}
validate_number_before
validate_number_after
validate_id_after
validate_model_after {'number': 5, 'id': 'abc'}
Create uuid
"""

Data validations using Pydantic make the codebase cleaner without having to explicitly perform validations within the function, and it can be done upon function instantiation – before the code even "enters" the function!

Hope you have gained a better understanding of the different validations available and how they are executed in Pydantic. This is just scratching the surface and there are more nuances to data validations, such as validating fields in subclasses, validating default values of class fields, and also the validation of data classes, functions, and more.


Related Links

Tags: Data Fastapi Pydantic Python Class Tips And Tricks

Comment