Pydantic v2 e Annotated: Criando Tipos de Domínio Reutilizáveis para Validação de Dados em Python

Published on: 2026-01-09
Post image
pt pydantic-v2 pydantic-annotated validacao-de-dados-python tipos-de-dominio-python pydantic-aftervalidator pydantic-field-validator pydantic-models validacao-reutilizavel-python annotated-python-typing pydantic-best-practices python-data-validate

A validação de dados é uma parte central de aplicações em Python, especialmente quando informações chegam de formulários, APIs e arquivos. Nesse contexto, a biblioteca Pydantic se tornou popular por permitir declarar modelos de dados com tipos e regras, garantindo consistência antes de usar esses valores em regras de negócio.

Na versão 2, o Pydantic passou a explorar com força o recurso Annotated do Python, permitindo combinar tipo e validação de forma reutilizável. O resultado é a redução de lógica repetida dentro de modelos e a criação de tipos de domínio, como “inteiro positivo” ou “slug”, que carregam regras junto do tipo.

Contexto: validação repetida e o custo de espalhar regras

Em muitos projetos, regras pequenas aparecem em vários lugares, como “precisa ser positivo” ou “não pode ter espaços”. Essas regras frequentemente são implementadas com funções de validação dentro de cada modelo, o que aumenta duplicação. Com o tempo, mudanças nessas regras exigem editar vários arquivos, elevando o risco de inconsistência. Além disso, a intenção do campo fica menos visível, porque a regra fica escondida em métodos separados. Essa combinação tende a gerar manutenção difícil e validação parcialmente divergente entre modelos.

O Pydantic usa modelos baseados em classes para validar e converter dados, e isso incentiva declarar campos com tipos. Porém, antes do uso amplo de Annotated no Pydantic v2, a forma típica de reaproveitar validações era limitada. Em geral, cada classe continha seus próprios field validators, isto é, funções que rodam para um campo específico. Esse formato funciona bem para casos isolados, mas perde eficiência quando a mesma regra aparece em muitos modelos. O problema principal não é a complexidade da validação, e sim sua duplicação e dispersão.

O “jeito antigo”: validadores repetidos em cada modelo

No padrão clássico, cada modelo define um método decorado para validar um campo. No Pydantic, um validador de campo é uma função associada a um atributo do modelo, executada durante a criação do objeto. Se a validação falhar, uma exceção é lançada e a instância não é criada. A seguir, o exemplo mostra a repetição típica ao validar valores positivos em modelos diferentes. Esse exemplo ilustra como a mesma lógica acaba copiada em classes distintas.

from pydantic import BaseModel, field_validator


class User(BaseModel):
    username: str
    age: int

    @field_validator("age")
    @classmethod
    def validar_positivo(cls, v: int) -> int:
        if v < 0:
            raise ValueError("Deve ser positivo")
        return v


class Product(BaseModel):
    name: str
    price: int

    @field_validator("price")
    @classmethod
    def validar_positivo(cls, v: int) -> int:
        if v < 0:
            raise ValueError("Deve ser positivo")
        return v

Esse arranjo cria um acoplamento direto entre regra e modelo, porque a regra “positivo” não existe como uma entidade reutilizável. Se o texto do erro mudar, ou se “zero” deixar de ser permitido, cada modelo precisa ser alterado. Em bases grandes, isso se transforma em “museu de validadores”, com funções quase iguais e nomes variados. A leitura do modelo também perde clareza, pois o campo aparece como int, mas suas regras reais estão escondidas em métodos. Na prática, esse padrão escala mal quando a validação é comum a muitos lugares.

O “jeito Annotated”: declarar tipos reutilizáveis com validação

O Annotated permite anexar metadados a um tipo, preservando o tipo original, mas acrescentando instruções extras. No Pydantic v2, esses metadados podem incluir validadores como AfterValidator, que executa uma função após o valor ser interpretado pelo tipo base. Em termos simples, a ideia vira “este campo é um int, e depois disso rode esta validação”. Isso torna a regra um componente reutilizável, definido uma vez e aplicado em muitos campos. O exemplo a seguir mostra a criação de um tipo PositiveInt reaproveitável.

from typing import Annotated
from pydantic import BaseModel, AfterValidator


def checar_positivo(v: int) -> int:
    if v < 0:
        raise ValueError("Deve ser positivo")
    return v


PositiveInt = Annotated[int, AfterValidator(checar_positivo)]


class User(BaseModel):
    username: str
    age: PositiveInt


class Product(BaseModel):
    name: str
    price: PositiveInt

A principal mudança é que a validação passa a morar em um “tipo de domínio”, e não espalhada em cada classe. A duplicação desaparece, pois a função checar_positivo é reutilizada por qualquer campo que use PositiveInt. O modelo fica mais expressivo, porque o tipo já comunica a intenção do campo. Outra vantagem é a manutenção: alterar a regra exige editar um único lugar, e todos os modelos passam a obedecer o novo comportamento. Esse padrão também melhora a consistência das mensagens de erro e reduz divergências acidentais.

Exemplo de erro e comportamento esperado na validação

Quando a validação falha, o Pydantic interrompe a criação do modelo e retorna uma exceção com detalhes. Esse comportamento é importante porque evita que o sistema opere com dados inválidos. A seguir aparece um exemplo de tentativa de criar um produto com preço negativo, o que viola a regra de PositiveInt. O trecho demonstra como a falha ocorre sem qualquer validador adicional dentro do modelo.

from pydantic import ValidationError

try:
    Product(name="Item Inválido", price=-50)
except ValidationError as e:
    print(str(e))

Nesse cenário, a falha acontece no momento da instanciação do modelo, antes que o valor seja usado em qualquer cálculo. O erro vem do encadeamento de validação montado pelo Annotated e aplicado ao campo. O modelo permanece limpo, sem métodos auxiliares, e a regra continua centralizada. Isso reduz a chance de existirem modelos “esquecidos” sem validação, já que a validação está acoplada ao tipo e não a uma classe específica. Com isso, a estrutura do código se aproxima de um vocabulário de domínio bem definido.

Empilhando validadores: composição de regras para tipos de domínio

Um benefício adicional do Annotated é permitir vários validadores em sequência, formando uma cadeia de transformações e checagens. No Pydantic v2, o AfterValidator pode ser aplicado mais de uma vez, e a saída de um validador alimenta o próximo. Isso facilita criar tipos “ricos”, como um Slug, que costuma exigir letras minúsculas e ausência de espaços. O exemplo abaixo monta um tipo que primeiro normaliza para minúsculas e depois rejeita strings com espaço. O objetivo é mostrar validação como composição, e não como cópia de código.

from typing import Annotated
from pydantic import BaseModel, AfterValidator


def para_minusculas(v: str) -> str:
    return v.lower()


def rejeitar_espacos(v: str) -> str:
    if " " in v:
        raise ValueError("Não deve conter espaços")
    return v


Slug = Annotated[str, AfterValidator(para_minusculas), AfterValidator(rejeitar_espacos)]


class BlogPost(BaseModel):
    title: str
    url_slug: Slug

Ao criar um objeto, a string fornecida passa pela normalização e depois pela checagem. Isso permite que “MY-DAY-TRIP” vire “my-day-trip” automaticamente, desde que não exista espaço. Em contrapartida, valores como “meu dia” falham, pois violam a regra de espaços. O tipo Slug passa a ser confiável e pode ser usado em qualquer modelo que precise dessa mesma semântica. Esse padrão também incentiva regras pequenas e focadas, com funções simples e fáceis de testar.

Combinação com metadados nativos do Pydantic: Field e restrições

Além de validadores personalizados, o Annotated aceita metadados nativos do Pydantic, como Field. O Field permite declarar restrições como gt (maior que), lt (menor que) e limites de tamanho, tornando parte das regras declarativas. A combinação de Field com AfterValidator produz tipos que expressam intervalo e regra específica ao mesmo tempo. O exemplo a seguir define um número que deve ser maior que 0, menor que 100 e também par. Essa mistura deixa as regras visíveis diretamente no tipo reutilizável.

from typing import Annotated
from pydantic import BaseModel, Field, AfterValidator


def checar_par(v: int) -> int:
    if v % 2 != 0:
        raise ValueError("Deve ser par")
    return v


SpecialNumber = Annotated[int, Field(gt=0, lt=100), AfterValidator(checar_par)]


class GameScore(BaseModel):
    score: SpecialNumber

A restrição de intervalo é aplicada pelo Field, enquanto a regra “par” vem do validador. Assim, valores como 88 passam, 89 falha por ser ímpar, e 150 falha por estar fora do intervalo. Essa abordagem também torna a intenção mais legível, pois o tipo já carrega o significado e as limitações. Em projetos, isso reduz a necessidade de procurar métodos de validação para entender o que um campo aceita. Com o tempo, o conjunto de tipos reutilizáveis vira um catálogo consistente de regras de entrada.

Tipos de domínio: expressividade, manutenção e padronização

Com Annotated, a validação deixa de ser “por campo em cada modelo” e passa a ser “por tipo de domínio”. Um tipo de domínio é um tipo que representa um conceito do problema, como Percentage, UserEmail ou NonEmptyString, junto das regras que o tornam válido. Isso aproxima o design de modelos de linguagens com “tipos refinados”, em que valores carregam invariantes. A consequência é um código mais expressivo: campos passam a comunicar semântica e não apenas formato. Também melhora a padronização, porque o mesmo conceito sempre valida do mesmo jeito em qualquer parte do sistema.

Na manutenção, a centralização das regras é o ganho mais visível. Alterar a regra de um Slug para também bloquear caracteres específicos, por exemplo, deixa de exigir uma busca por modelos afetados. Em times, isso reduz divergências de implementação, como mensagens diferentes para o mesmo erro ou limites ligeiramente distintos. A distribuição de regras por dezenas de classes dá lugar a um conjunto menor de tipos bem nomeados. Esse arranjo diminui repetição e aumenta previsibilidade, mantendo os modelos mais curtos e focados em descrever dados.

Encerramento: por que Annotated muda o desenho de modelos no Pydantic v2

O Annotated no Pydantic v2 transforma validação em algo composável, reutilizável e explícito. Regras que antes ficavam espalhadas em métodos dentro de cada modelo passam a viver em tipos de domínio, definidos uma vez e usados em todo o código. A possibilidade de empilhar AfterValidator e combinar com Field cria um sistema de validação declarativo, com menos duplicação e maior clareza. Isso fortalece a consistência, pois o mesmo conceito sempre valida de forma idêntica, independentemente do modelo. Como resultado, modelos ficam mais limpos, regras ficam centralizadas, e o desenho do sistema se torna mais estável e fácil de manter.