Pydantic v2: validar dados de entrada em Python
1. Visão geral
Dados de requisições HTTP, arquivos JSON ou message queues frequentemente têm tipos errados, campos faltando ou valores sem sentido. Se você verificar tudo com if/else espalhado pelo código, ele rapidamente fica confuso. Pydantic v2 permite descrever a “forma” dos dados com type hints e depois fazer parse automático, coerção de tipo e reportar erros detalhados.
Este artigo passa pelas principais ferramentas do Pydantic v2: BaseModel, Field, field_validator, model_validator, ConfigDict, TypeAdapter. No final, vemos como escrever testes com pytest. Depois de ler, você será capaz de construir seus próprios modelos validando entradas de forma segura para sua aplicação.
2. Instalação
Este artigo usa Python 3.11+ e Pydantic 2.x:
pip install "pydantic>=2.7" pytest
Se você precisar do tipo EmailStr, instale também a extensão:
pip install "pydantic[email]"
3. Modelo básico com BaseModel
3.1. Declaração e coerção de tipo
Um modelo é uma classe que herda de BaseModel. Cada atributo com type hint é um field:
from pydantic import BaseModel
class Customer(BaseModel):
id: int
name: str
email: str
is_active: bool = True
customer = Customer(id="42", name="An", email="an@example.com")
print(customer)
print(customer.id, type(customer.id))
id=42 name='An' email='an@example.com' is_active=True
42 <class 'int'>
Você passa id="42" como string, mas Pydantic converte automaticamente para int. Este é o modo lax, o padrão: Pydantic faz coerção de tipo quando a conversão é razoável. O field is_active tem um valor padrão, então não é obrigatório passar.
3.2. Tratando ValidationError
Quando os dados estão errados, Pydantic lança ValidationError e coleta todos os erros de uma vez, não parando no primeiro:
from pydantic import ValidationError
try:
Customer(id="abc", name="An")
except ValidationError as e:
print(e.error_count())
for err in e.errors():
print(err["loc"], err["type"], err["msg"])
2
('id',) int_parsing Input should be a valid integer, unable to parse string as an integer
('email',) missing Field required
Cada erro é um dict contendo loc (caminho do field), type (código de erro estável) e msg (mensagem legível). Ao retornar erros para o cliente, você deve confiar em loc e type. O conteúdo de msg pode mudar entre versões.
4. Restrições de valor com Field e Annotated
Type hints só verificam tipo. Para limitar valores, você usa Field:
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, Field
Quantity = Annotated[int, Field(gt=0, le=100)]
class OrderItem(BaseModel):
sku: str = Field(pattern=r"^[A-Z]{3}-\d{4}$")
quantity: Quantity
unit_price: Decimal = Field(gt=0, max_digits=10, decimal_places=2)
item = OrderItem(sku="ABC-1234", quantity=3, unit_price="19.90")
print(item)
sku='ABC-1234' quantity=3 unit_price=Decimal('19.90')
Explicação:
patternforçaskua corresponder à regex, exemploABC-1234.Quantityé um tipo reutilizável graças aAnnotated. Todo field declarado comoQuantitydeve estar no intervalo(0, 100].unit_priceusaDecimalpara evitar erros de ponto flutuante. O valor deve ser positivo e ter no máximo 2 casas decimais.
Tente passar dados ruins:
try:
OrderItem(sku="abc", quantity=0, unit_price="10")
except ValidationError as e:
for err in e.errors():
print(err["loc"], err["type"])
('sku',) string_pattern_mismatch
('quantity',) greater_than
Outras restrições úteis: min_length/max_length para strings e listas, ge/lt para números, multiple_of para múltiplos.
5. Validadores personalizados com field_validator
Quando restrições são mais complexas que regex ou faixas, você escreve um validador customizado:
from pydantic import BaseModel, Field, field_validator
class Contact(BaseModel):
name: str
phone: str = Field(pattern=r"^0\d{9}$")
@field_validator("name")
@classmethod
def clean_name(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("name não pode estar vazio")
return v.title()
@field_validator("phone", mode="before")
@classmethod
def normalize_phone(cls, v):
if isinstance(v, str):
v = v.replace(" ", "").replace(".", "")
if v.startswith("+84"):
v = "0" + v[3:]
return v
contact = Contact(name=" nguyễn văn an ", phone="+84 912 345 678")
print(contact)
name='Nguyễn Văn An' phone='0912345678'
Há dois modos importantes:
mode="after"(padrão), como emclean_name: executa depois que Pydantic validou o tipo, entãové garantidamente umastr.mode="before", como emnormalize_phone: executa antes da validação de tipo epattern.vpode ser qualquer coisa, então você precisa verificarisinstance. Aqui normalizamos+84 912 345 678para0912345678, e entãopatterné aplicado.
Validadores devem retornar um valor. O valor retornado é o que fica salvo no field. Para reportar erro, você lança ValueError ou AssertionError. Pydantic o embrulha em um erro value_error, com a mensagem "Value error, name não pode estar vazio".
Nota: A ordem dos decoradores importa muito.
@field_validatordeve estar acima de@classmethod. Se inverter, você terá um erro confuso ao definir a classe.
6. Validando múltiplos campos simultaneamente com model_validator
Algumas regras envolvem múltiplos fields, por exemplo, a data de check-out deve ser depois da data de check-in. Neste caso, você precisa de model_validator:
from datetime import date
from typing import Self
from pydantic import BaseModel, ValidationError, model_validator
class Booking(BaseModel):
room_id: int
check_in: date
check_out: date
@model_validator(mode="after")
def check_dates(self) -> Self:
if self.check_out <= self.check_in:
raise ValueError("check_out deve ser após check_in")
return self
try:
Booking(room_id=101, check_in="2026-10-20", check_out="2026-10-18")
except ValidationError as e:
err = e.errors()[0]
print(err["loc"], err["msg"])
() Value error, check_out deve ser após check_in
Com mode="after", o validador recebe uma instância já validada em cada field. Você acessa self.check_in como um atributo normal e deve fazer return self. O erro tem loc vazio () porque pertence ao modelo inteiro, não a um field específico. A string "2026-10-20" também é automaticamente parseada para date.
7. Configurando modelo com ConfigDict
model_config controla o comportamento geral do modelo. Esta é a configuração comum para dados sensíveis como pagamentos:
from pydantic import BaseModel, ConfigDict, ValidationError
class PaymentRequest(BaseModel):
model_config = ConfigDict(
extra="forbid",
strict=True,
str_strip_whitespace=True,
frozen=True,
)
order_id: int
amount: int
currency: str
try:
PaymentRequest(order_id="1", amount=50000, currency="VND", coupon="FREE")
except ValidationError as e:
for err in e.errors():
print(err["loc"], err["type"])
('order_id',) int_type
('coupon',) extra_forbidden
O significado de cada opção:
extra="forbid": rejeita fields desconhecidos comocoupon. O padrão é"ignore", ou seja, Pydantic silenciosamente ignora fields extras. Isso pode esconder erros de digitação no lado do cliente.strict=True: desativa coerção."1"não será mais aceito comoint.str_strip_whitespace=True: remove espaços em branco das duas pontas automaticamente.frozen=True: a instância é imutável. Atribuirpayment.amount = 0lançará erro.
Se você só quer strict para alguns fields, use Field(strict=True) ou tipos como StrictInt, StrictStr.
Outra opção importante é validate_assignment=True. Por padrão, Pydantic não valida quando você reatribui um atributo após criar o objeto. Ativar esta opção mantém o modelo sempre válido.
8. Validando a partir de dict, JSON e tipos arbitrários
Na prática, dados geralmente chegam como dict ou string JSON. Pydantic v2 oferece dois class methods:
raw_json = '{"id": 7, "name": "Bình", "email": "binh@example.com"}'
customer = Customer.model_validate_json(raw_json)
raw_dict = {"id": 8, "name": "Chi", "email": "chi@example.com", "is_active": False}
customer2 = Customer.model_validate(raw_dict)
print(customer.model_dump())
print(customer2.model_dump_json())
{'id': 7, 'name': 'Bình', 'email': 'binh@example.com', 'is_active': True}
{"id":8,"name":"Chi","email":"chi@example.com","is_active":false}
Se a entrada é uma string JSON, você deve usar model_validate_json em vez de json.loads seguido de model_validate. Este é mais rápido porque o núcleo Rust do Pydantic faz o parse e validação em uma única passagem. No sentido inverso, model_dump() retorna um dict, enquanto model_dump_json() retorna uma string JSON.
Quando você precisa validar um tipo que não é um modelo, como list[OrderItem], use TypeAdapter:
from pydantic import TypeAdapter
items_adapter = TypeAdapter(list[OrderItem])
items = items_adapter.validate_python([
{"sku": "ABC-0001", "quantity": 2, "unit_price": "5.00"},
{"sku": "XYZ-0002", "quantity": 1, "unit_price": "12.50"},
])
print(len(items), items[1].unit_price)
2 12.50
Você deve criar o TypeAdapter uma vez no nível do módulo e reutilizá-lo. A inicialização tem custo de construção de schema, não deve ser repetida a cada requisição.
9. Escrevendo testes com pytest
Validação é lógica de negócio e deve ser testada. Suponha que os modelos acima estejam em um arquivo models.py:
from decimal import Decimal
import pytest
from pydantic import ValidationError
from models import Booking, Contact, OrderItem
def test_order_item_coerces_valid_input():
item = OrderItem(sku="ABC-1234", quantity="3", unit_price="19.90")
assert item.quantity == 3
assert item.unit_price == Decimal("19.90")
@pytest.mark.parametrize("quantity", [0, -1, 101])
def test_order_item_rejects_out_of_range_quantity(quantity):
with pytest.raises(ValidationError) as exc_info:
OrderItem(sku="ABC-1234", quantity=quantity, unit_price="10")
error = exc_info.value.errors()[0]
assert error["loc"] == ("quantity",)
assert error["type"] in {"greater_than", "less_than_equal"}
def test_contact_normalizes_phone():
contact = Contact(name="an", phone="+84 912.345.678")
assert contact.phone == "0912345678"
assert contact.name == "An"
def test_booking_rejects_checkout_before_checkin():
with pytest.raises(ValidationError, match="check_out deve ser após check_in"):
Booking(room_id=1, check_in="2026-10-20", check_out="2026-10-20")
Execute os testes:
pytest -q
..... [100%]
5 passed
Note como os testes fazem assert em loc e type, não comparando o msg literalmente. Assim, testes não quebram quando Pydantic muda a redação da mensagem em uma nova versão. parametrize ajuda você a cobrir ambos os limites 0 e 101 com uma única função.
10. Erros comuns e quando não usar
Usar a API errada da v1. Muita documentação antiga ainda usa sintaxe da v1. Aqui está uma tabela rápida de correspondência:
v1 v2
@validator @field_validator
@root_validator @model_validator
class Config: ... model_config = ConfigDict(...)
.dict() / .json() .model_dump() / .model_dump_json()
.parse_obj() / .parse_raw() .model_validate() / .model_validate_json()
O código antigo ainda funciona mas emite DeprecationWarning. Você deve migrar completamente para a API nova.
Surpresas com modo lax. Modo lax aceita "yes" como True e 1.0 como 1. Com dados financeiros ou IDs, este comportamento pode ser indesejado. Quando é o caso, ative strict.
Esquecer return no validador. Um field_validator sem return deixa o field receber None. Um model_validator(mode="after") faltando return self também causa erro.
Pular validação sem saber. model_construct() cria uma instância sem passar por validação. Este método é apenas para dados em que você confia absolutamente, por exemplo, leitura direto do seu banco de dados.
Nota: Não use Pydantic para todo objeto interno. Em loops quentes processando milhões de objetos já limpos, o custo de validação é desnecessário. Nesses casos,
dataclassouNamedTuplesão mais apropriados. Coloque Pydantic na borda do seu sistema: APIs, arquivos de configuração, mensagens de outros sistemas.
11. Conclusão
Pydantic v2 transforma validação de dados de entrada em declaração em vez de imperativo. Você descreve tipos com type hints, adiciona restrições com Field e Annotated, escreve regras customizadas com field_validator e model_validator. Comportamento geral como proibir fields extras ou desativar coerção é controlado via ConfigDict.
Três pontos para lembrar: use model_validate_json para entrada JSON, ative extra="forbid" com dados importantes, e teste validação baseando-se em loc/type em vez de conteúdo de mensagem. Quando aplicado corretamente, na borda do seu sistema, Pydantic garante que o núcleo da sua aplicação só trabalha com dados limpos e bem-tipados.