Pydantic e Tipos Avançados: Personalizando a Validação de Dados

Cansado de programar?

Conheça a melhor e mais completa formação de Python e Django e sinta-se um programador verdadeiramente competente. Além de Python e Django, você também vai aprender Banco de Dados, SQL, HTML, CSS, Javascript, Bootstrap e muito mais!

Quero aprender Python e Django de Verdade! Quero aprender!
Suporte

Tire suas dúvidas diretamente com o professor

Projetos práticos

Projetos práticos voltados para o mercado de trabalho

Prática profissional

Formação moderna com foco na prática profissional

Download do Artigo

✅ Artigo atualizado em Setembro 2026 O artigo dizia ser de Pydantic V2, mas vários exemplos usavam a API do V1 (@validator, @root_validator, regex=, class Config, example=), que no V2 gera avisos ou erro. Todos foram migrados para a API atual e executados no Pydantic 2.13, com as saídas reais.

Salve salve Pythonista :wave:

A validação de dados é essencial no desenvolvimento de aplicações robustas em Python.

Com o Pydantic, é possível personalizar a validação de dados utilizando tipos avançados e mecanismos poderosos (se está começando agora, leia antes a introdução ao Pydantic).

Neste artigo, abordaremos como trabalhar com tipos complexos como Decimal, UUID e DateTime, utilizar campos opcionais e valores padrão, implementar validações personalizadas com field_validator e model_validator, e introduzir o uso de Field para configurar validações e metadados.

Dominar esses tópicos é crucial para garantir a integridade e a consistência dos dados em suas aplicações Python.

Vá Direto ao Assunto…

Trabalhando com Tipos Complexos: Decimal, UUID e DateTime

O Pydantic oferece suporte a diversos tipos complexos que facilitam a manipulação de dados específicos.

Vamos explorar como utilizar Decimal, UUID e DateTime em seus modelos.

Utilizando Decimal

O tipo Decimal é útil para representar números com precisão decimal, evitando problemas de arredondamento comuns com floats.

1
2
3
4
5
6
from decimal import Decimal
from pydantic import BaseModel

class Produto(BaseModel):
    nome: str
    preco: Decimal

Explicação do código:

  1. Importação: Importamos Decimal do módulo decimal e BaseModel do Pydantic.
  2. Definição da Classe: Criamos a classe Produto herdando de BaseModel.
  3. Atributos: Definimos nome como string e preco como Decimal.

:gear: Falando em Pydantic: O Ebookr.ai usa Pydantic extensivamente para validar e estruturar conteúdo gerado por IA. É uma plataforma que criei para gerar ebooks profissionais sobre qualquer tema - com infográficos automáticos, capa gerada por IA e exportação em PDF. Conhece lá!

Utilizando UUID

Para identificar objetos de forma única, o UUID (Universally Unique Identifier) é uma excelente escolha.

1
2
3
4
5
6
import uuid
from pydantic import BaseModel

class Usuario(BaseModel):
    id: uuid.UUID
    nome: str

Explicação do código:

  1. Importação: Importamos uuid e BaseModel.
  2. Definição da Classe: Criamos a classe Usuario com id do tipo UUID e nome como string.

Antes de continuar… Está curtindo esse conteúdo? :thumbsup:

Que tal receber 30 dias de conteúdo direto na sua Caixa de Entrada?

Sua assinatura não pôde ser validada.
Você fez sua assinatura com sucesso.

Assine as PyDicas e receba 30 dias do melhor conteúdo Python na sua Caixa de Entrada: direto e sem enrolação!

Utilizando DateTime

O tipo DateTime permite trabalhar com data e hora de forma eficiente (veja mais sobre o módulo no post de manipulação de datas e horários no Python).

1
2
3
4
5
6
from datetime import datetime
from pydantic import BaseModel

class Evento(BaseModel):
    nome: str
    data_hora: datetime

Explicação do código:

  1. Importação: Importamos datetime do módulo datetime e BaseModel.
  2. Definição da Classe: Criamos a classe Evento com nome e data_hora como DateTime.

Utilizando Campos Opcionais (Optional) e Valores Padrão

Nem todos os campos dos modelos precisam ser obrigatórios. O Pydantic facilita a definição de campos opcionais e a atribuição de valores padrão.

Campos Opcionais

Usamos Optional para indicar que um campo pode ser ignorado ou ter valor None.

1
2
3
4
5
6
from typing import Optional
from pydantic import BaseModel

class Cliente(BaseModel):
    nome: str
    email: Optional[str] = None

Explicação do código:

  1. Importação: Importamos Optional de typing e BaseModel.
  2. Definição da Classe: Criamos Cliente com nome obrigatório e email opcional, com valor padrão None.

Valores Padrão

Atribuímos valores padrão diretamente nos campos para garantir que tenham um valor caso não sejam fornecidos.

1
2
3
4
5
from pydantic import BaseModel

class Configuracao(BaseModel):
    ativo: bool = True
    nivel_acesso: int = 1

Explicação do código:

  1. Definição da Classe: Criamos Configuracao com ativo como True e nivel_acesso como 1 por padrão.

Validação Personalizada com Validator

Embora o Pydantic valide automaticamente muitos tipos, às vezes é necessário implementar validações específicas. No Pydantic V2, o decorador @field_validator (usado junto com @classmethod) facilita essa customização. O antigo @validator do V1 ainda existe só por compatibilidade: está depreciado e será removido no V3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pydantic import BaseModel, field_validator
from decimal import Decimal

class Pedido(BaseModel):
    quantidade: int
    preco_unitario: Decimal

    @field_validator('quantidade')
    @classmethod
    def quantidade_positiva(cls, v: int) -> int:
        if v <= 0:
            raise ValueError('A quantidade deve ser positiva')
        return v

    @field_validator('preco_unitario')
    @classmethod
    def preco_valido(cls, v: Decimal) -> Decimal:
        if v <= Decimal('0.00'):
            raise ValueError('O preço unitário deve ser maior que zero')
        return v

Explicação do código:

  1. Definição da Classe: Criamos Pedido com quantidade e preco_unitario.
  2. Validador de Quantidade:
    • O método quantidade_positiva verifica se quantidade é maior que zero.
    • Se não, lança um ValueError.
  3. Validador de Preço Unitário:
    • O método preco_valido assegura que preco_unitario seja maior que zero.
    • Se não, lança um ValueError.

Testando com uma quantidade inválida:

1
2
3
4
5
6
7
8
from pydantic import ValidationError

print(Pedido(quantidade=3, preco_unitario="19.90"))

try:
    Pedido(quantidade=0, preco_unitario="19.90")
except ValidationError as e:
    print(e)

E a saída será:

1
2
3
4
5
quantidade=3 preco_unitario=Decimal('19.90')
1 validation error for Pedido
quantidade
  Value error, A quantidade deve ser positiva [type=value_error, input_value=0, input_type=int]
    For further information visit https://errors.pydantic.dev/2.13/v/value_error

Repare que a string "19.90" foi convertida para Decimal antes de chegar ao validador, e que o ValueError levantado vira um ValidationError com o tipo value_error. O número no link (2.13) é a versão do Pydantic instalada.

Esses validadores garantem que os dados atendam às regras de negócio definidas.

Introdução ao Field para Configurar Validações e Metadados

O Field do Pydantic permite configurar validações adicionais e adicionar metadados aos campos dos modelos, como descrições, títulos e exemplos.

1
2
3
4
5
6
7
8
from decimal import Decimal
from uuid import UUID
from pydantic import BaseModel, Field

class Produto(BaseModel):
    id: UUID = Field(..., title="ID do Produto", description="Identificador único do produto")
    nome: str = Field(..., min_length=3, max_length=50, examples=["Caneca"])
    preco: Decimal = Field(..., gt=0, description="Preço do produto em reais")

Explicação do código:

  1. Importação: Importamos Field de pydantic, além de outros tipos necessários.
  2. Definição da Classe: Criamos Produto com id, nome e preco.
  3. Uso de Field:
    • id: Define título e descrição para documentação.
    • nome: Define comprimento mínimo e máximo, além de exemplos (examples). No V2, Field não aceita mais argumentos soltos como example=: use examples=[...] ou, para qualquer outro dado extra no schema, json_schema_extra={...}.
    • preco: Define que o valor deve ser maior que zero e adiciona uma descrição.

O Field é especialmente útil para gerar documentação automática e garantir que os campos atendam a critérios específicos além da validação básica de tipos.

Casos Práticos com Tipos Avançados

1. Sistema de Pagamentos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from pydantic import BaseModel, ConfigDict, Field, field_validator
from decimal import Decimal
from datetime import datetime
from typing import Optional
from uuid import UUID, uuid4
from enum import Enum

class StatusPagamento(str, Enum):
    PENDENTE = "pendente"
    PROCESSANDO = "processando"
    APROVADO = "aprovado"
    REJEITADO = "rejeitado"

class Pagamento(BaseModel):
    model_config = ConfigDict(use_enum_values=True)

    id: UUID = Field(default_factory=uuid4)
    valor: Decimal = Field(..., gt=0, decimal_places=2)
    moeda: str = Field(default="BRL", pattern=r"^[A-Z]{3}$")
    status: StatusPagamento = StatusPagamento.PENDENTE
    created_at: datetime = Field(default_factory=datetime.now)
    processado_em: Optional[datetime] = None

    @field_validator('valor')
    @classmethod
    def validar_valor_maximo(cls, v: Decimal) -> Decimal:
        if v > Decimal('999999.99'):
            raise ValueError('Valor excede limite de R$ 999.999,99')
        return v

# Uso
pagamento = Pagamento(valor=Decimal('150.50'))
print(pagamento.model_dump_json(indent=2))

E a saída será algo como (o id e o created_at mudam a cada execução):

1
2
3
4
5
6
7
8
{
  "id": "492d022a-a6b6-48f0-ba12-a1b81095a4be",
  "valor": "150.50",
  "moeda": "BRL",
  "status": "pendente",
  "created_at": "2026-09-27T10:54:40.693557",
  "processado_em": null
}

O que mudou em relação à API do V1: regex= virou pattern= (o regex= agora gera erro ao definir a classe), @validator virou @field_validator e a class Config interna virou o atributo model_config = ConfigDict(...). Como use_enum_values=True, o status é guardado como a string "pendente", e o Decimal é serializado como string no JSON para não perder precisão.

Uma moeda fora do padrão de três letras maiúsculas é recusada pelo pattern:

1
2
3
4
5
6
from pydantic import ValidationError

try:
    Pagamento(valor=Decimal('10.00'), moeda="real")
except ValidationError as e:
    print(e)

E a saída será:

1
2
3
4
1 validation error for Pagamento
moeda
  String should match pattern '^[A-Z]{3}$' [type=string_pattern_mismatch, input_value='real', input_type=str]
    For further information visit https://errors.pydantic.dev/2.13/v/string_pattern_mismatch

2. Agendamento de Eventos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator
from datetime import datetime, timedelta

class Evento(BaseModel):
    titulo: str = Field(..., min_length=5, max_length=100)
    inicio: datetime
    fim: datetime
    timezone: str = Field(default="America/Sao_Paulo")
    participantes_min: int = Field(default=1, ge=1)
    participantes_max: int = Field(..., ge=1, le=1000)

    @field_validator('fim')
    @classmethod
    def fim_depois_inicio(cls, v: datetime, info: ValidationInfo) -> datetime:
        inicio = info.data.get('inicio')
        if inicio and v <= inicio:
            raise ValueError('Fim deve ser após início')
        return v

    @field_validator('participantes_max')
    @classmethod
    def max_maior_que_min(cls, v: int, info: ValidationInfo) -> int:
        minimo = info.data.get('participantes_min')
        if minimo is not None and v < minimo:
            raise ValueError('Máximo deve ser >= mínimo')
        return v

    @model_validator(mode='after')
    def validar_duracao(self) -> 'Evento':
        if self.fim - self.inicio > timedelta(hours=24):
            raise ValueError('Evento não pode durar mais de 24h')
        return self

No V2, o validador de campo que depende de outro campo recebe um ValidationInfo e lê os campos já validados em info.data (no V1 era o argumento values). Os campos são validados na ordem em que foram declarados, por isso fim consegue ver inicio. Já o antigo @root_validator virou @model_validator(mode='after'), que recebe a instância pronta (self) e deve devolvê-la. Sem migrar, o @root_validator do exemplo original nem chega a funcionar no V2: a classe falha ao ser definida, exigindo skip_on_failure=True.

Testando com um evento de dois dias:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pydantic import ValidationError

dados = {
    "titulo": "Python Summit",
    "inicio": "2026-10-10T09:00:00",
    "fim": "2026-10-12T18:00:00",  # dois dias depois
    "participantes_min": 10,
}

for maximo in (5, 50):
    try:
        Evento(**dados, participantes_max=maximo)
    except ValidationError as e:
        print(e)

E a saída será:

1
2
3
4
5
6
7
1 validation error for Evento
participantes_max
  Value error, Máximo deve ser >= mínimo [type=value_error, input_value=5, input_type=int]
    For further information visit https://errors.pydantic.dev/2.13/v/value_error
1 validation error for Evento
  Value error, Evento não pode durar mais de 24h [type=value_error, input_value={'titulo': 'Python Summit...'participantes_max': 50}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.13/v/value_error

Na primeira tentativa, só aparece o erro de participantes_max: o model_validator(mode='after') roda apenas quando todos os campos passaram na validação. Com o máximo corrigido, a regra de duração entra em ação, e o erro aparece sem nome de campo, porque pertence ao modelo inteiro.

Documentar uma API bem é resultado de fundamentos sólidos em Python; é essa base, do zero ao Django e às APIs REST, que a Jornada Python constrói com você:

3. Documentação de API com Field

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import json
from pydantic import BaseModel, ConfigDict, Field, EmailStr, HttpUrl
from typing import List, Optional

class UsuarioAPI(BaseModel):
    """Schema para criação de usuário via API"""

    model_config = ConfigDict(
        json_schema_extra={
            "examples": [
                {
                    "nome": "João Silva",
                    "email": "[email protected]",
                    "idade": 25,
                    "site": "https://joao.com",
                    "tags": ["python", "dev"]
                }
            ]
        }
    )

    nome: str = Field(
        ...,
        min_length=2,
        max_length=100,
        title="Nome Completo",
        description="Nome completo do usuário",
        examples=["João Silva"]
    )

    email: EmailStr = Field(
        ...,
        title="E-mail",
        description="E-mail válido do usuário",
        examples=["[email protected]"]
    )

    idade: int = Field(
        ...,
        ge=18,
        le=120,
        title="Idade",
        description="Idade do usuário (18-120)",
        examples=[25]
    )

    site: Optional[HttpUrl] = Field(
        None,
        title="Website",
        description="URL do site pessoal",
        examples=["https://joao.com"]
    )

    tags: List[str] = Field(
        default_factory=list,
        max_length=10,
        title="Tags",
        description="Lista de tags (máx 10)",
        examples=[["python", "dev"]]
    )

# Gerar schema JSON para documentação OpenAPI
print(json.dumps(UsuarioAPI.model_json_schema(), indent=2, ensure_ascii=False))

No V2, schema_json() virou model_json_schema() (que devolve um dicionário, por isso o json.dumps), max_items virou max_length, example= virou examples=[...] e schema_extra na class Config virou json_schema_extra no model_config. O início da saída é este (o schema completo tem 87 linhas):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "description": "Schema para criação de usuário via API",
  "examples": [
    {
      "email": "[email protected]",
      "idade": 25,
      "nome": "João Silva",
      "site": "https://joao.com",
      "tags": [
        "python",
        "dev"
      ]
    }
  ],
  "properties": {
    "nome": {
      "description": "Nome completo do usuário",
      "examples": [
        "João Silva"
      ],
      "maxLength": 100,
      "minLength": 2,
      "title": "Nome Completo",
      "type": "string"
    },

Como usa EmailStr, este exemplo precisa do pacote email-validator (pip install "pydantic[email]").

Tipos Avançados: Comparação

Tipo Quando Usar Validação Exemplo
Decimal Valores monetários, precisão Precisão decimal exata Preços, salários
UUID IDs únicos, chaves primárias Formato UUID válido IDs de banco
DateTime Timestamps, agendamentos Formato ISO 8601 created_at
EmailStr E-mails Formato de e-mail Cadastros
HttpUrl URLs URL válida Links externos
IPvAnyAddress Endereços IP IPv4/IPv6 Logs, segurança
Json JSON em string JSON válido Configs dinâmicas
SecretStr Senhas, tokens - Credenciais

Validators: Níveis de Validação

Os trechos abaixo ficam dentro da classe do modelo e usam a API do Pydantic V2 (from pydantic import field_validator, model_validator).

Field-level validator

1
2
3
4
5
@field_validator('campo')
@classmethod
def validar_campo(cls, v):
    # Valida um campo específico
    return v

Root validator (validar múltiplos campos)

“Root validator” é o nome do V1. No V2, @root_validator está depreciado e o equivalente é @model_validator. Com mode='after', ele recebe a instância já validada:

1
2
3
4
@model_validator(mode='after')
def validar_modelo_completo(self):
    # Valida relações entre campos
    return self

Validator com pre=True (antes da validação de tipo)

pre=True era a opção do @validator do V1. No V2, o equivalente é mode='before' no @field_validator: o validador recebe o valor bruto, antes da conversão de tipo.

1
2
3
4
5
@field_validator('campo', mode='before')
@classmethod
def processar_antes(cls, v):
    # Processa antes da conversão de tipo
    return v

Quando Usar Validators Customizados

✅ Regras de negócio complexas Ex: CEP deve ser válido para o estado

✅ Validações com dependências Ex: data_fim > data_inicio

✅ Normalização de dados Ex: converter telefone para formato padrão

✅ Integração com APIs externas Ex: validar CPF consultando serviço

Quando NÃO Usar

❌ Validações simples de tipo Type hints já resolvem

❌ Performance crítica Validators adicionam overhead

❌ Lógica de negócio pesada Melhor em camada de serviço

Conclusão

Neste artigo, exploramos como o Pydantic permite personalizar a validação de dados utilizando tipos avançados como Decimal, UUID e DateTime.

Aprendemos a definir campos opcionais e atribuir valores padrão, implementar validações personalizadas com os decoradores @field_validator e @model_validator, e utilizar o Field para configurar validações adicionais e metadados.

Essas ferramentas tornam o gerenciamento de dados em Python mais eficiente e seguro, fortalecendo a integridade das suas aplicações.

Com esses conhecimentos, você está pronto para criar modelos robustos e confiáveis em seus projetos Python. Para combinar vários modelos entre si, veja também o post sobre modelos complexos no Pydantic.

Quer aprofundar a base de Python e tipagem em projetos guiados? Conheça a Jornada Python, nosso curso completo de Python.

Começe agora sua Jornada na Programação!

Não deixe para amanhã o sucesso que você pode começar a construir hoje!

#newsletter Olá :wave: Curtiu o artigo? Então faça parte da nossa Newsletter! Privacidade Não se preocupe, respeitamos sua privacidade. Você pode se descadastrar a qualquer momento.