Pular para o conteúdo

    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:

    basico/cap07_pydantic_detalhe.pylinhas 10 a 43
    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())
    
    Saída
    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:

    basico/cap07_pydantic_detalhe.pylinhas 45 a 47
    ruim = {**valido, "endereco": {"cidade": "Delhi", "cep": "123"}}
    erro = cliente.post("/usuarios", json=ruim).json()["detail"][0]
    print(erro["loc"], erro["type"])
    
    Saída
    ['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":

    basico/cap07_pydantic_detalhe.pylinhas 52 a 69
    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"])
    
    Saída
    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:

    basico/cap07_pydantic_detalhe.pylinhas 74 a 80
    class Post(BaseModel):
        tags: list[str] = Field(default_factory=list)
    
    
    a, b = Post(), Post()
    a.tags.append("novo")
    print(a.tags, b.tags)
    
    Saída
    ['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:

    basico/cap07_pydantic_detalhe.pylinhas 85 a 109
    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"])
    
    Saída
    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:

    basico/cap07_pydantic_detalhe.pylinhas 114 a 126
    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"}))
    
    Saída
    [{'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.