Pydantic v2 é uma biblioteca Python voltada para validação e serialização de dados com base em tipos. Em APIs, isso significa transformar entradas “cruas” (JSON, strings, números) em objetos consistentes, garantindo regras como formato, limites e campos obrigatórios. A versão 2 introduz mudanças que deixam os modelos mais rápidos, explícitos e fáceis de manter, especialmente quando o esquema da API cresce.
As melhorias centrais de Pydantic v2 ajudam a reduzir “magia” implícita e a tornar as intenções mais claras no código. O resultado costuma ser um contrato de API mais estável, validações previsíveis e geração de esquema (JSON Schema/OpenAPI) mais fiel ao que realmente é aceito e produzido. A seguir estão sete recursos que simplificam esquemas de API e aumentam a segurança do contrato.
TypeAdapter: validação sem criar um modelo
Nem toda validação precisa de uma classe baseada em modelo, como BaseModel. Em muitos fluxos, existe apenas um fragmento de dado a validar, como uma lista, um tipo primitivo ou um pedaço de payload. O TypeAdapter resolve isso ao validar e converter tipos diretamente, reaproveitando o núcleo de performance de Pydantic v2. Isso reduz boilerplate e evita “poluir” a camada de modelos com classes descartáveis.
Esse recurso é útil em integrações com mensageria, cache e webhooks, onde chegam partes do contrato fora do fluxo HTTP tradicional. Também ajuda a padronizar validações reutilizáveis em pontos diferentes do sistema, sem duplicar regras. A validação ocorre por métodos consistentes, como validate_python, e a saída pode ser serializada com alta performance. O trecho abaixo mostra uma lista de inteiros positivos validada sem nenhum modelo.
from typing import Annotated, List
from pydantic import TypeAdapter, Field
# Tipo reutilizável: inteiro com restrição de ser maior que zero
IntPositivo = Annotated[int, Field(gt=0)]
# Adaptador para lista de inteiros positivos
adaptador = TypeAdapter(List[IntPositivo])
dados = adaptador.validate_python(["3", 4, 5]) # converte e valida
json_bytes = adaptador.dump_json(dados) # serializa em bytes (bom para resposta)
Annotated + Field: restrições ao lado do tipo
Pydantic v2 incentiva declarar restrições diretamente junto do tipo usando Annotated, um recurso do typing do Python. Isso permite “anexar” metadados de validação ao tipo, como tamanho mínimo, regex, limites numéricos e múltiplos. A leitura fica mais direta, pois tipo e regra aparecem no mesmo lugar. Além disso, o JSON Schema gerado tende a ficar mais limpo e previsível.
Esse estilo também reduz situações em que regras ficam espalhadas em validadores ou em configurações distantes. Em revisões de código, a restrição é visível imediatamente, o que diminui erros de interpretação. Em contratos de API, o esquema gerado reflete essas regras com mais fidelidade. O exemplo abaixo define e reutiliza tipos anotados para e-mail, preço e SKU.
from typing import Annotated
from pydantic import BaseModel, Field
Email = Annotated[str, Field(pattern=r".+@.+\..+", max_length=254)]
Preco = Annotated[float, Field(ge=0, multiple_of=0.01)]
class ItemLinha(BaseModel):
sku: Annotated[str, Field(min_length=3, max_length=32)]
price: Preco
buyer_email: Email
esquema = ItemLinha.model_json_schema()
Validadores determinísticos: field_validator e model_validator
Validação costuma crescer com o tempo, e por isso a previsibilidade vira requisito. Pydantic v2 torna a intenção mais clara separando validações de campo e validações do modelo inteiro. O decorador field_validator trata regras localizadas em um atributo específico, enquanto model_validator valida relações entre campos. Também existe o controle de modo, como “before” e “after”, que define quando a validação ocorre.
Essa separação diminui efeitos colaterais e facilita refatorações, pois cada regra tem lugar claro. Normalizações comuns, como remover espaços e padronizar caixa de texto, ficam organizadas por campo. Já regras de consistência, como exigir um campo se outro tiver certo valor, ficam no validador do modelo. O exemplo a seguir normaliza e-mail e exige código de convite para um domínio específico.
from pydantic import BaseModel, field_validator, model_validator
class Cadastro(BaseModel):
email: str
password: str
invite_code: str | None = None
@field_validator("email")
@classmethod
def normalizar_email(cls, v: str) -> str:
return v.strip().lower()
@model_validator(mode="after")
def validar_convite(self):
if self.email.endswith("@vip.example") and not self.invite_code:
raise ValueError("invite_code obrigatório para domínio VIP")
return self
Serialização precisa: field_serializer e model_serializer
APIs frequentemente precisam de uma forma de representação externa diferente da estrutura interna do sistema. Pydantic v2 fornece ganchos de serialização explícitos para controlar como campos e modelos inteiros viram JSON. O field_serializer ajusta a saída de um campo específico, e o model_serializer permite redefinir o formato do objeto completo. Com isso, a camada de controller deixa de “remendar” dicionários após o dump.
Um caso comum é lidar com datas, que internamente são objetos datetime, mas no contrato de API devem aparecer em texto no padrão ISO 8601. Outro caso recorrente é ocultar campos internos ou reestruturar a resposta pública. A regra fica centralizada no modelo, reduzindo divergência entre endpoints. O código abaixo serializa datas em UTC e cria uma visão pública no nível do modelo.
from datetime import datetime, timezone
from pydantic import BaseModel, field_serializer, model_serializer
class Auditoria(BaseModel):
actor_id: int
at: datetime
@field_serializer("at", when_used="json")
@classmethod
def em_iso_utc(cls, v: datetime) -> str:
return v.astimezone(timezone.utc).isoformat()
class Usuario(BaseModel):
id: int
email: str
audit: Auditoria
@model_serializer(mode="json")
def como_publico(self):
return {
"id": self.id,
"email": self.email,
"last_action": self.audit.at,
}
computed_field: campos derivados e documentados
Respostas de API muitas vezes incluem campos “derivados”, calculados a partir de outros valores. Pydantic v2 formaliza isso com computed_field, que declara explicitamente um campo calculado e o inclui na serialização e no esquema. Isso evita divergências entre o que é retornado na prática e o que está documentado. O campo calculado também se mantém próximo da lógica que o define.
Esse recurso reduz duplicação, pois elimina cálculos repetidos em diferentes endpoints. A manutenção melhora, já que o comportamento do campo derivado fica centralizado. A documentação gerada tende a refletir melhor a resposta real. O exemplo abaixo cria um campo de assentos restantes, sempre não negativo.
from pydantic import BaseModel, computed_field
class Plano(BaseModel):
name: str
seats: int
used: int
@computed_field
def seats_left(self) -> int:
return max(self.seats - self.used, 0)
RootModel[T]: encapsular um tipo sem boilerplate
Alguns endpoints retornam “somente uma lista” ou “somente um valor”, sem um objeto com chaves. Pydantic v2 permite isso com RootModel, que envolve qualquer tipo diretamente, mantendo tipagem e esquema. Isso evita criar modelos artificiais com campos como items apenas para acomodar uma lista. O corpo da resposta fica mais limpo e o contrato menos confuso.
Esse padrão é comum em endpoints de listagem, webhooks que enviam arrays e corpos que são primitivos fortes, como tokens em texto. Mesmo sem um objeto “envelope”, a validação continua completa, incluindo validação de itens internos. A serialização segue os métodos padrão do v2. O exemplo a seguir define uma lista tipada de usuários como raiz.
from typing import List
from pydantic import BaseModel, RootModel
class Usuario(BaseModel):
id: int
email: str
class Usuarios(RootModel[List[Usuario]]):
pass
payload = Usuarios.model_validate([{"id": 1, "email": "a@b.com"}])
payload_json = payload.model_dump_json()
ConfigDict e núcleo mais rápido: configuração única e explícita
Pydantic v2 consolida configurações em ConfigDict, aplicado via model_config. Isso torna explícitas decisões como permitir ou proibir campos extras e se o modelo deve ser estrito. O modo strict reduz coerções automáticas inesperadas, como converter string “1” em inteiro 1, o que pode esconder problemas no cliente. O conjunto de configurações também melhora consistência entre modelos do mesmo serviço.
Além das configurações, o caminho crítico de validação e serialização é acelerado por pydantic-core, um núcleo otimizado. Em APIs com payloads grandes ou aninhamento profundo, isso tende a reduzir CPU e latência. Também existem métodos padronizados, como model_validate, model_dump e model_dump_json, que substituem nomes antigos e trazem uniformidade. O trecho abaixo mostra um modelo com regras típicas de contrato e segurança de entrada.
from pydantic import BaseModel, ConfigDict, Field
class Pagamento(BaseModel):
model_config = ConfigDict(
extra="forbid", # proíbe campos desconhecidos
strict=True, # valida com menos coerção automática
frozen=False, # permite mutação quando necessário
ser_json_bytes=True, # serializa JSON como bytes (útil no transporte)
)
id: int
amount: float = Field(ge=0, multiple_of=0.01)
Padrão integrado: modelo escalável com validação, serialização e lista tipada
Um padrão prático em serviços maiores combina tipos anotados reutilizáveis, validação explícita, serialização controlada e respostas de lista com raiz tipada. Isso reduz repetição e mantém o contrato consistente em diferentes endpoints. A criação do esquema e a validação seguem os mesmos mecanismos, diminuindo desvio entre documentação e comportamento. O exemplo abaixo reúne as peças principais em um conjunto pequeno, mas realista.
O código demonstra restrições ao lado dos tipos, normalização de e-mail, serialização de data em ISO UTC, um campo calculado e um wrapper de lista com RootModel. Também usa configuração para proibir campos extras e operar em modo estrito. A validação de entrada acontece com model_validate, e a saída JSON é gerada com model_dump_json. Essa combinação costuma produzir contratos estáveis e evolutivos.
from typing import Annotated, List
from datetime import datetime, timezone
from pydantic import (
BaseModel,
Field,
ConfigDict,
field_validator,
field_serializer,
computed_field,
RootModel,
)
UserId = Annotated[int, Field(ge=1)]
Email = Annotated[str, Field(pattern=r".+@.+\..+", max_length=254)]
Role = Annotated[str, Field(pattern=r"(admin|editor|viewer)")]
class Usuario(BaseModel):
model_config = ConfigDict(extra="forbid", strict=True)
id: UserId
email: Email
created_at: datetime
role: Role
@field_validator("email")
@classmethod
def normalizar_email(cls, v: str) -> str:
return v.strip().lower()
@field_serializer("created_at", when_used="json")
@classmethod
def data_em_iso_utc(cls, v: datetime) -> str:
return v.astimezone(timezone.utc).isoformat()
@computed_field
def is_staff(self) -> bool:
return self.role in {"admin", "editor"}
class Usuarios(RootModel[List[Usuario]]):
pass
u = Usuario.model_validate(
{
"id": 42,
"email": " PERSON@EXAMPLE.COM ",
"created_at": "2025-10-06T04:30:00Z",
"role": "editor",
}
)
body_bytes = Usuarios([u]).model_dump_json()
Conclusão
Pydantic v2 simplifica esquemas de API ao privilegiar ferramentas explícitas e consistentes. TypeAdapter reduz boilerplate quando apenas partes do payload precisam de validação, e Annotated aproxima regras do tipo que as define. Validadores com field_validator e model_validator deixam a execução previsível, enquanto serializadores com field_serializer e model_serializer consolidam o contrato de saída no próprio modelo.
Campos derivados ficam formais com computed_field, respostas sem envelope se tornam limpas com RootModel, e a configuração com ConfigDict reforça consistência e rigor. Com métodos unificados como model_validate e model_dump_json, o fluxo de entrada e saída se torna mais padronizado. Em conjunto, esses recursos ajudam a construir contratos mais seguros, documentáveis e fáceis de evoluir, mesmo quando a API deixa de ser pequena.