Capítulo 7, Básico
Modelos Pydantic em detalhe
O modelo é o contrato dos seus dados. Restrições, modelos aninhados e validadores deixam a entrada limpa antes de a sua função começar a rodar.
Restrições e modelos aninhados
O Field impõe regras a cada campo (tamanho, faixa, padrão de texto). E um modelo pode conter outro, o que é muito comum em dados reais, como um usuário com um endereço:
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class Endereco(BaseModel):
cidade: str
cep: str = Field(pattern=r"^\d{5}-?\d{3}$")
class Usuario(BaseModel):
nome: str = Field(min_length=2, max_length=50)
idade: int = Field(ge=0, le=150)
email: str = Field(pattern=r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
endereco: Endereco
tags: list[str] = Field(default_factory=list)
@app.post("/usuarios")
def criar_usuario(usuario: Usuario):
return usuario
cliente = TestClient(app)
valido = {
"nome": "Mohit",
"idade": 25,
"email": "mohit@exemplo.com",
"endereco": {"cidade": "Delhi", "cep": "20010-100"},
}
resposta = cliente.post("/usuarios", json=valido)
print(resposta.status_code, resposta.json())
200 {'nome': 'Mohit', 'idade': 25, 'email': 'mohit@exemplo.com', 'endereco': {'cidade': 'Delhi', 'cep': '20010-100'}, 'tags': []}
A resposta espelha a estrutura aninhada. Quando o erro está dentro do modelo interno, o loc mostra o caminho completo até ele:
ruim = {**valido, "endereco": {"cidade": "Delhi", "cep": "123"}}
erro = cliente.post("/usuarios", json=ruim).json()["detail"][0]
print(erro["loc"], erro["type"])
['body', 'endereco', 'cep'] string_pattern_mismatch
Campos extras são ignorados, a não ser que você proíba
Um equívoco comum é achar que o Pydantic recusa campos que o modelo não declara. Por padrão, ele ignora os extras em silêncio. Para recusá-los, configure extra="forbid":
from pydantic import ConfigDict, ValidationError
class Aberto(BaseModel):
nome: str
class Fechado(BaseModel):
model_config = ConfigDict(extra="forbid")
nome: str
dados = {"nome": "Ana", "campo_extra": 1}
print(hasattr(Aberto(**dados), "campo_extra"))
try:
Fechado(**dados)
except ValidationError as erro:
print(erro.errors()[0]["type"], erro.errors()[0]["loc"])
False
extra_forbidden ('campo_extra',)
Recusar extras ajuda a pegar erros de digitação do cliente (idad em vez de idade), que de outro modo passariam sem aviso.
Padrões mutáveis são seguros no Pydantic
Em uma classe comum, tags = [] como padrão é uma armadilha clássica, porque a lista é compartilhada. O Pydantic copia o padrão para cada instância, e o default_factory é a forma explícita de dizer isso:
class Post(BaseModel):
tags: list[str] = Field(default_factory=list)
a, b = Post(), Post()
a.tags.append("novo")
print(a.tags, b.tags)
['novo'] []
Validadores
Quando uma regra não cabe em um Field, escreva um validador. O field_validator age sobre um campo, e o model_validator age sobre o conjunto, útil para regras que comparam campos:
from pydantic import field_validator, model_validator
class Cadastro(BaseModel):
nome: str
senha: str = Field(min_length=8)
confirmar_senha: str
@field_validator("nome")
@classmethod
def limpar_nome(cls, valor: str) -> str:
return valor.strip()
@model_validator(mode="after")
def senhas_iguais(self):
if self.senha != self.confirmar_senha:
raise ValueError("as senhas não conferem")
return self
print(Cadastro(nome=" Ana ", senha="segredo123", confirmar_senha="segredo123").nome)
try:
Cadastro(nome="Ana", senha="segredo123", confirmar_senha="outra-coisa")
except ValidationError as erro:
print(erro.errors()[0]["msg"])
Ana
Value error, as senhas não conferem
Exemplos na documentação e saída do modelo
Um exemplo no modelo aparece em /docs e deixa o teste manual mais rápido. E o model_dump aceita opções para controlar o que sai:
class Produto(BaseModel):
model_config = ConfigDict(
json_schema_extra={"examples": [{"nome": "Caneta", "preco": 3.5, "estoque": 100}]}
)
nome: str
preco: float
estoque: int = 0
print(Produto.model_json_schema()["examples"])
produto = Produto(nome="Caneta", preco=3.5)
print(produto.model_dump(exclude_unset=True))
print(produto.model_dump(exclude={"estoque"}))
[{'estoque': 100, 'nome': 'Caneta', 'preco': 3.5}]
{'nome': 'Caneta', 'preco': 3.5}
{'nome': 'Caneta', 'preco': 3.5}
O exclude_unset=True devolve só o que o cliente informou, e não os padrões. Esse detalhe é a base do PATCH, que o próximo capítulo usa.
Modelo de entrada e modelo de saída são coisas diferentes
É tentador usar o mesmo modelo para receber e devolver. Eu evito: a entrada não tem
id(quem cria é o servidor), e a saída não tem senha. O capítulo 10 mostra como separar os dois.
Exercício 1
Um modelo de produto estrito
Crie ProdutoNovo com nome (mínimo 2 caracteres), preco (maior que zero) e estoque (inteiro, mínimo 0, padrão 0), recusando campos extras. Confira os casos válido e inválidos.