Pydantic v2 na Prática: 7 Recursos que Simplificam Schemas de API e Escalam com Confiança

Published on: 2025-12-30
Post image
pt pydantic-v2 fastapi-schemas contratos-de-api validacao-de-dados-python pydantic-performance api-schemas-python pydantic-typeadapter openapi-fastapi backend-python engenharia-de-software-backend

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.