Corpo da Requisição e Validação de Dados — Guia Prático Aprofundado do Pydantic V2
Validação de dados é como a segurança do aeroporto—cada passageiro (requisição) deve passar por verificação de identidade (verificação de tipo), inspeção de bagagem (validação de restrições), e revisão de sua declaração (validadores personalizados) antes de embarcar no avião (entrar na lógica de negócios).
1. O Que Você Vai Aprender
- Pydantic V2
BaseModel:field_validator/model_validatorsubstitui@validatordo V1 - Restrições Avançadas do
Field():gt/lt/pattern/examplese Geração de JSON Schema - Modelos Aninhados e Combinações de Modelos: Aplicações Práticas de
Optional,Union, eLiteral model_config:ConfigDictsubstituiclass Configefrom_attributes=Trueno modo ORM do V1- Cenário da Alice: Design completo do modelo Pydantic para envios de preços de produtos do PriceTracker
2. A História Real da Alice
(1) Problema: Envio e Validação de Dados de Preço Cheios de Erros
O PriceTracker da Alice recebe dados de preços enviados por fornecedores, exigindo que os preços sejam maiores que 0, que as moedas sejam especificadas usando códigos de três letras ISO 4217, e que os nomes dos produtos não possam estar vazios. No entanto, o front end do Bob ocasionalmente envia preços negativos ou moedas inválidas. O código de validação personalizado escrito em Flask está espalhado por 10 locais diferentes, e ignora o caso extremo onde "o preço é 0," resultando em dados sujos com preço 0 no banco de dados.
(2) Uma Solução para Validação Declarativa no Pydantic V2
O Pydantic V2 substitui a validação imperativa por um modelo declarativo; todas as restrições são definidas dentro do modelo, e uma vez definidas, aplicam-se globalmente.
from pydantic import BaseModel, Field, field_validator
class PriceCreate(BaseModel):
product_id: int = Field(gt=0)
price: float = Field(gt=0, description="Price in USD")
currency: str = Field(pattern=r"^[A-Z]{3}$", examples=["USD", "EUR"])
@field_validator("currency")
@classmethod
def validate_currency(cls, v: str) -> str:
if v not in {"USD", "EUR", "GBP", "JPY", "CNY"}:
raise ValueError(f"Unsupported currency: {v}")
return v
(3) Resultado
O código de validação de preço foi consolidado de 10 blocos de lógica separados em uma única definição de modelo; valores de 0 e preços negativos são filtrados automaticamente, e a validação de moeda é garantida por uma abordagem de dupla camada combinando expressões regulares e validadores personalizados, eliminando dados sujos do banco de dados.
3. Fluxo de Dados do Pydantic V2
(1) Ciclo de Vida Completo
flowchart TD
A[JSON Request Body] --> B[model_validate]
B --> C[Type Coercion]
C --> D[field_validator]
D --> E[model_validator]
E --> F[Valid Model Instance]
F --> G[model_dump]
G --> H[JSON Response]
F --> I[model_dump_json]
I --> J[JSON String]
| Fase | Método | Descrição |
|---|---|---|
| Análise de Entrada | model_validate(data) |
Analisar e validar a partir de dict/JSON |
| Conversão de Tipo | Automática | "42" → 42, "9.99" → 9.99 |
| Validação de Campo | @field_validator |
Validação Personalizada para um Único Campo |
| Validação de Modelo | @model_validator |
Validação Conjunta de Campos Cruzados |
| Serialização de Saída | model_dump() / model_dump_json() |
Converter Modelo para dict/JSON |
(1) ▶ Exemplo: Modelo Base do Pydantic V2
from pydantic import BaseModel, Field
class ProductCreate(BaseModel):
name: str = Field(min_length=1, max_length=200)
category: str = Field(max_length=100)
base_price: float = Field(gt=0, description="Preço base em USD")
# Analisar e validar
data = {"name": "Widget", "category": "electronics", "base_price": 29.99}
product = ProductCreate.model_validate(data)
print(product.model_dump())
Saída:
{'name': 'Widget', 'category': 'electronics', 'base_price': 29.99}
4. field_validator e model_validator
(1) Comparação de Migração V1 → V2
flowchart LR
V1[Pydantic V1] --> Migrate[V1 → V2 Migration]
Migrate --> V2[Pydantic V2]
V1 --- A["@validator('field')"]
V2 --- B["@field_validator('field')"]
V1 --- C["@root_validator"]
V2 --- D["@model_validator(mode='after')"]
V1 --- E["class Config:"]
V2 --- F["model_config = ConfigDict(...)"]
V1 --- G[".dict()"]
V2 --- H[".model_dump()"]
| Sintaxe V1 | Sintaxe V2 | Descrição |
|---|---|---|
@validator("field") |
@field_validator("field") |
Validador de Campo |
@root_validator |
@model_validator(mode="after") |
Validação em Nível de Modelo |
class Config: orm_mode = True |
model_config = ConfigDict(from_attributes=True) |
Modelo ORM |
.dict() |
.model_dump() |
Serializar para dict |
.json() |
.model_dump_json() |
Serializar para JSON |
(1) ▶ Exemplo: field_validator—Validação de Campo Único
from pydantic import BaseModel, Field, field_validator
class PriceCreate(BaseModel):
product_id: int = Field(gt=0)
price: float = Field(gt=0)
currency: str = Field(default="USD", max_length=3)
@field_validator("price")
@classmethod
def price_precision(cls, v: float) -> float:
# Arredondar para 2 casas decimais
return round(v, 2)
@field_validator("currency")
@classmethod
def valid_currency(cls, v: str) -> str:
allowed = {"USD", "EUR", "GBP", "JPY", "CNY"}
if v not in allowed:
raise ValueError(f"Currency must be one of {allowed}")
return v.upper()
# Testar validação
p = PriceCreate(product_id=1, price=9.999, currency="usd")
print(p.model_dump())
Saída:
{'product_id': 1, 'price': 10.0, 'currency': 'USD'}
(2) ▶ Exemplo: model_validator—validação de campos cruzados
from pydantic import BaseModel, Field, model_validator
class PriceRangeQuery(BaseModel):
min_price: float = Field(ge=0, description="Preço mín em USD")
max_price: float = Field(ge=0, description="Preço máx em USD")
@model_validator(mode="after")
def check_range(self):
if self.min_price > self.max_price:
raise ValueError("min_price must be <= max_price")
return self
# Válido
valid = PriceRangeQuery(min_price=10, max_price=100)
print(valid.model_dump())
# Inválido - gera erro de validação
# PriceRangeQuery(min_price=100, max_price=10)
Saída:
{'min_price': 10.0, 'max_price': 100.0}
5. Restrições Avançadas do Field() e JSON Schema
(1) Referência Rápida dos Parâmetros do Field()
| Parâmetro | Tipo | Descrição | Mapeamento JSON Schema |
|---|---|---|---|
gt |
Valor | Maior que | exclusiveMinimum |
ge |
Valor | Maior ou igual a | minimum |
lt |
Valor | Menor que | exclusiveMaximum |
le |
Valor | Menor ou igual a | maximum |
min_length |
String | Comprimento Mínimo | minLength |
max_length |
String | Comprimento Máximo | maxLength |
pattern |
String | Expressão Regular | pattern |
default |
Qualquer | Padrão | default |
examples |
Lista | Valores de Exemplo | examples |
description |
String | Descrição | description |
alias |
String | Alias do Campo | Mapeamento de Alias |
(1) ▶ Exemplo: Restrições do Field() e JSON Schema
from pydantic import BaseModel, Field
class ProductCreate(BaseModel):
name: str = Field(
min_length=1,
max_length=200,
description="Nome de exibição do produto",
examples=["Wireless Mouse", "USB Cable"],
)
sku: str = Field(
pattern=r"^[A-Z]{2}-\d{4,6}$",
description="Código SKU: 2 letras + 4-6 dígitos",
examples=["EL-1234", "CB-567890"],
)
base_price: float = Field(
gt=0,
le=999999.99,
description="Preço base em USD",
examples=[9.99, 49.99, 199.99],
)
# Visualizar JSON Schema gerado
print(ProductCreate.model_json_schema())
Saída:
# Execução Bem-sucedida
6. Modelos Aninhados e Combinações de Modelos
(1) Estrutura de Modelo Aninhado
(1) ▶ Exemplo: Modelo aninhado do PriceTracker
from pydantic import BaseModel, Field
from typing import Optional
class PriceInfo(BaseModel):
amount: float = Field(gt=0, description="Valor do preço em USD")
currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")
source: str = Field(max_length=100, description="Fonte do preço")
class ProductCreate(BaseModel):
name: str = Field(min_length=1, max_length=200)
category: str = Field(max_length=100)
current_price: PriceInfo # Modelo aninhado
original_price: Optional[PriceInfo] = None # Aninhado opcional
# Validação aninhada
data = {
"name": "Wireless Mouse",
"category": "electronics",
"current_price": {"amount": 29.99, "currency": "USD", "source": "Amazon"},
"original_price": {"amount": 49.99, "currency": "USD", "source": "Amazon"},
}
product = ProductCreate.model_validate(data)
print(product.model_dump())
Saída:
# Execução Bem-sucedida
(2) ▶ Exemplo: Union e Literal
from pydantic import BaseModel, Field
from typing import Union, Literal
class SinglePrice(BaseModel):
type: Literal["single"] = "single"
amount: float = Field(gt=0)
class RangePrice(BaseModel):
type: Literal["range"] = "range"
min_amount: float = Field(gt=0)
max_amount: float = Field(gt=0)
class ProductPrice(BaseModel):
product_id: int = Field(gt=0)
pricing: Union[SinglePrice, RangePrice] # União discriminada
# FastAPI usa o campo "type" para determinar qual modelo validar
data = {"product_id": 1, "pricing": {"type": "range", "min_amount": 10, "max_amount": 50}}
pp = ProductPrice.model_validate(data)
print(pp.model_dump())
Saída:
# Execução Bem-sucedida
(2) Configuração ConfigDict
(3) ▶ Exemplo: model_config e o padrão ORM
from pydantic import BaseModel, ConfigDict
class ProductResponse(BaseModel):
model_config = ConfigDict(
from_attributes=True, # Habilitar modo ORM (ler de objetos SQLAlchemy)
populate_by_name=True, # Permitir tanto nome do campo quanto alias
json_schema_extra={
"examples": [{"id": 1, "name": "Widget", "price": 9.99}]
},
)
id: int
name: str
price: float
# Com from_attributes=True, pode criar a partir de atributos de objeto
class FakeORMObject:
def __init__(self):
self.id = 1
self.name = "Widget"
self.price = 9.99
orm_obj = FakeORMObject()
response = ProductResponse.model_validate(orm_obj)
print(response.model_dump())
Saída:
{'id': 1, 'name': 'Widget', 'price': 9.99}
7. Exemplo Completo
A validação de campo e validação de campos cruzados do Pydantic V2, combinados com o padrão ORM e corpos de requisição do FastAPI, permitem um fluxo de trabalho completo de validação de entrada de dados.
from fastapi import FastAPI
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict
class PriceCreate(BaseModel):
product_name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, description="O preço deve ser maior que 0")
currency: str = "USD"
@field_validator("currency")
@classmethod
def validate_currency(cls, v: str) -> str:
if v not in ("USD", "EUR", "GBP"):
raise ValueError("currency must be USD/EUR/GBP")
return v
class PriceRangeQuery(BaseModel):
min_price: float = Field(ge=0)
max_price: float = Field(ge=0)
@model_validator(mode="after")
def validate_range(self):
if self.min_price > self.max_price:
raise ValueError("min_price must <= max_price")
return self
app = FastAPI()
@app.post("/prices")
async def create_price(data: PriceCreate):
return data.model_dump()
Saída:
POST /prices {"product_name":"Widget","price":9.99} → {"product_name":"Widget","price":9.99,"currency":"USD"}
POST /prices {"product_name":"","price":-1} → 422 Validation Error
❓ Perguntas Frequentes
field_validator e model_validator?field_validator para validação de campo único (como verificações de formato), e use model_validator para validação de campos cruzados (como min_price <= max_price).from_attributes=True faz?examples do Field() e json_schema_extra?examples é uma lista de valores de exemplo para um campo, mapeada para exemplos OpenAPI; json_schema_extra é uma propriedade de schema adicional em nível de modelo.📖 Resumo
- Pydantic V2 substitui
@validatore@root_validatordo V1 por@field_validatore@model_validator Field()mapeia automaticamente restrições para JSON Schema, enquanto impulsiona tanto a documentação OpenAPI quanto a validação de dados da requisição- O modelo aninhado suporta combinações flexíveis de
Optional,Union, eLiteral, enquantoLiteralimplementa um tipo conjunto discriminativo ConfigDict(from_attributes=True)habilita o modo ORM para criar modelos Pydantic diretamente de objetos SQLAlchemy- Núcleo da Migração V1→V2:
.dict()→.model_dump(),class Config→model_config = ConfigDict(...)
📝 Exercícios
- Problema Básico (Dificuldade ⭐): Crie o modelo
PriceCreate, que incluiproduct_id: int(> 0) eprice: float(> 0), e verifique que entrada inválida aciona um erro de validação. Dica:BaseModel+Field(gt=0) - Exercício Avançado (Dificuldade ⭐⭐): Adicione ao
PriceCreateum@field_validatorpara validar que o campocurrencysó pode conter USD/EUR/GBP, e adicione@model_validatorpara garantirmin_price <= max_price. Dica:field_validator+model_validator(mode="after") - Desafio (Dificuldade ⭐⭐⭐): Design um sistema de modelo aninhado completo para o PriceTracker:
ProductCreatecontémPriceInfo(amount + currency); a currency emPriceInfoé restringida por uma expressão regular; e o SKU emProductCreateé restringido pelo formato empattern. Dica:Field(pattern=...)+BaseModelaninhado
---|



