Capítulo 02: Tipagem e Serialização com Pydantic v2 (BaseModel, Field)
Especialização em Backend com Python & FastAPI • FastAPI & Python 3.12+ • Pydantic v2, SQLModel, Async/Await e Microsserviços
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Dados Brutos (JSON, Query, Body, Headers)"] --> B["Pydantic-Core (Validador compilado em Rust)"]
B --> C{"Execução de Tipagem"}
C -->|"Validação bem-sucedida"| D["Instância BaseModel Tipada e Imutável"]
C -->|"Falha de Restrição / Tipo"| E["ValidationError Estruturado (HTTP 422 Unprocessable)"]
D --> F["Regras de Negócio: field_validator e model_validator"]
F --> G["Transformação de Saída: field_serializer"]
G --> H["Serialização Final: model_dump() / model_dump_json()"]
subgraph RUST["Núcleo Rust (Pydantic v2)"]
B
C
end
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#ffebee,stroke:#e53935,stroke-width:2px
style F fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style G fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
style H fill:#e0f2f1,stroke:#009688,stroke-width:2px
🏛️ 1. Pydantic v2: A Nova Era com Núcleo Rust (pydantic-core)
O Pydantic é o motor de validação e serialização que transformou o ecossistema Python moderno. Na versão 2 (v2), toda a lógica interna de parsing, validação de tipos e travessia de árvores de dados foi reescrita em Rust através da biblioteca pydantic-core. Essa reengenharia conferiu ganhos de performance de 5x a 50x em comparação com a versão 1 puramente em Python, mantendo tipagem estática rigorosa e compatibilidade com PEP 484, PEP 563 e PEP 604.
No FastAPI, os modelos do Pydantic desempenham um papel triplo:
- Contrato de Entrada: Validam e coagem parâmetros de query, headers, cookies e bodies de requisição antes da função de rota ser acionada.
- Documentação Automática: Geram o JSON Schema padrão da especificação OpenAPI 3.1 e as páginas interativas do Swagger UI e Redoc.
- Contrato de Saída: Filtram e formatam os dados de resposta (
response_model), garantindo que campos privados não vazem para clientes externos.
Tabela de Migração Padrão: Pydantic v1 vs Pydantic v2
| Recurso / Operação | Sintaxe Legada (v1) | Sintaxe Canônica (v2) | Finalidade e Impacto |
|---|---|---|---|
| Dicionário Python | obj.dict() |
obj.model_dump() |
Converte o modelo para dicionário nativo Python |
| String JSON | obj.json() |
obj.model_dump_json() |
Serialização direta ultra-rápida via C/Rust |
| Parsing de Dicionário | Model.parse_obj(data) |
Model.model_validate(data) |
Valida dados de um dicionário arbitrário |
| Configuração | class Config: interna |
model_config = ConfigDict(...) |
Configuração tipada e autocompletável por IDEs |
| Validador de Campo | @validator("campo") |
@field_validator("campo") |
Exige @classmethod explícito e suporta mode |
| Validador de Raiz | @root_validator |
@model_validator(mode="after") |
Permite validações cruzadas entre múltiplos atributos |
⚙️ 2. Restrições Estruturais com Field
A função Field() permite declarar regras de validação semânticas e metadados de documentação OpenAPI diretamente na definição de cada atributo:
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, Field, EmailStr
class ItemCarrinho(BaseModel):
# Field(...) ou Field() sem valor default marca o campo como obrigatório
sku: str = Field(
...,
pattern=r"^[A-Z]{3}-\d{4}$",
description="Código identificador do produto (ex: PRO-1024)",
examples=["ELE-9821"]
)
quantidade: int = Field(
...,
gt=0,
le=100,
description="Quantidade adquirida (mínimo 1, máximo 100)"
)
preco_unitario: Decimal = Field(
...,
ge=Decimal("0.01"),
decimal_places=2,
description="Valor monetário preciso com 2 casas decimais"
)
tags: list[str] = Field(
default_factory=list,
max_length=5,
description="Lista de tags opcionais (máximo 5 itens)"
)
Atenção: Nunca utilize literais mutáveis como valor default (ex:
tags: list[str] = []), pois a lista seria compartilhada entre instâncias. Utilize sempredefault_factory=listoudefault_factory=dict.
🔍 3. Ciclo de Validação: field_validator e model_validator
Os validadores no Pydantic v2 oferecem controle cirúrgico sobre a ordem de execução através dos modos mode="before" e mode="after".
mode="before": Roda sobre os dados brutos antes de qualquer coerção de tipo do Pydantic (útil para normalização de strings, split de texto, etc.).mode="after"(Padrão): Roda após o Pydantic ter feito o cast seguro dos tipos primitivos.
from datetime import date
from pydantic import BaseModel, Field, field_validator, model_validator, ValidationInfo
class PeriodoReserva(BaseModel):
codigo_reserva: str
data_checkin: date
data_checkout: date
hospedes_adultos: int = Field(..., gt=0)
hospedes_criancas: int = Field(default=0, ge=0)
# 1. Validador de Campo Único (Roda em cada atributo isolado)
@field_validator("codigo_reserva", mode="before")
@classmethod
def normalizar_codigo(cls, valor: str) -> str:
if isinstance(valor, str):
return valor.strip().upper()
raise ValueError("O código de reserva deve ser uma string válida.")
# 2. Validador de Modelo (Validação Cruzada entre múltiplos campos)
@model_validator(mode="after")
def validar_consistencia_reserva(self) -> "PeriodoReserva":
if self.data_checkout <= self.data_checkin:
raise ValueError("A data de checkout deve ser estritamente posterior à data de check-in.")
total_hospedes = self.hospedes_adultos + self.hospedes_criancas
if total_hospedes > 8:
raise ValueError("O limite máximo de hóspedes por acomodação é de 8 pessoas.")
return self
🎨 4. Serialização Customizada e Controle com ConfigDict
Enquanto a validação cuida da entrada, a serialização cuida da representação JSON enviada aos clientes. O decorador @field_serializer permite mascarar dados sensíveis, formatar números ou serializar objetos não primitivos:
from pydantic import BaseModel, ConfigDict, field_serializer, EmailStr
class UsuarioResponse(BaseModel):
# ConfigDict define políticas rigorosas de integridade
model_config = ConfigDict(
frozen=True, # Torna instâncias imutáveis (hashable e thread-safe)
extra="forbid", # Rejeita campos desconhecidos com HTTP 422
str_strip_whitespace=True # Remove espaços acidentais no início e fim de strings
)
id: int
nome_completo: str
email: EmailStr
saldo_centavos: int
# Transforma o campo saldo_centavos em formatação monetária legível no JSON
@field_serializer("saldo_centavos")
def formatar_moeda(self, centavos: int) -> str:
return f"R$ {centavos / 100:.2f}"
# Mascara o e-mail para privacidade LGPD/GDPR
@field_serializer("email")
def mascarar_email(self, email: EmailStr) -> str:
partes = str(email).split("@")
usuario, dominio = partes[0], partes[1]
mascara = usuario[0] + "***" + usuario[-1] if len(usuario) > 2 else "***"
return f"{mascara}@{dominio}"