Pydantic already converts and validates declared types; custom validators should cover domain rules the schema cannot express alone. field_validator handles fields, while model_validator evaluates the object as a whole.

from datetime import date
from typing import Self
from pydantic import BaseModel, field_validator, model_validator

class Periodo(BaseModel):
    inicio: date
    fim: date

    @field_validator("inicio", "fim", mode="before")
    @classmethod
    def limpar_data(cls, valor: object) -> object:
        return valor.strip() if isinstance(valor, str) else valor

    @model_validator(mode="after")
    def validar_ordem(self) -> Self:
        if self.fim < self.inicio:
            raise ValueError("fim deve ser posterior ao início")
        return self

A before validator receives raw input and must handle any object. The after model validator receives the validated instance and returns self. Date ordering is a cross-field rule, so it belongs at model level.

Practical guidance

Keep validators deterministic and free from network or database calls. Raise ValueError with a useful message that does not expose secrets. Avoid mutating input before raising inside unions because another branch may receive the changed object. Test valid input, boundaries, and failures.

Start with the Pydantic guide and separate configuration using Pydantic Settings.

The official Pydantic validators documentation, accessed July 22, 2026, documents the API and its constraints.