404 Not Found

404 Not Found


nginx

Corpo da Requisição e Validação de Dados — Guia Prático Aprofundado do Pydantic V2

Validação de dados é como a segurança do aeroporto—cada passageiro (requisição) deve passar por verificação de identidade (verificação de tipo), inspeção de bagagem (validação de restrições), e revisão de sua declaração (validadores personalizados) antes de embarcar no avião (entrar na lógica de negócios).

1. O Que Você Vai Aprender


2. A História Real da Alice

(1) Problema: Envio e Validação de Dados de Preço Cheios de Erros

O PriceTracker da Alice recebe dados de preços enviados por fornecedores, exigindo que os preços sejam maiores que 0, que as moedas sejam especificadas usando códigos de três letras ISO 4217, e que os nomes dos produtos não possam estar vazios. No entanto, o front end do Bob ocasionalmente envia preços negativos ou moedas inválidas. O código de validação personalizado escrito em Flask está espalhado por 10 locais diferentes, e ignora o caso extremo onde "o preço é 0," resultando em dados sujos com preço 0 no banco de dados.

(2) Uma Solução para Validação Declarativa no Pydantic V2

O Pydantic V2 substitui a validação imperativa por um modelo declarativo; todas as restrições são definidas dentro do modelo, e uma vez definidas, aplicam-se globalmente.

PYTHON
from pydantic import BaseModel, Field, field_validator

class PriceCreate(BaseModel):
    product_id: int = Field(gt=0)
    price: float = Field(gt=0, description="Price in USD")
    currency: str = Field(pattern=r"^[A-Z]{3}$", examples=["USD", "EUR"])

    @field_validator("currency")
    @classmethod
    def validate_currency(cls, v: str) -> str:
        if v not in {"USD", "EUR", "GBP", "JPY", "CNY"}:
            raise ValueError(f"Unsupported currency: {v}")
        return v

(3) Resultado

O código de validação de preço foi consolidado de 10 blocos de lógica separados em uma única definição de modelo; valores de 0 e preços negativos são filtrados automaticamente, e a validação de moeda é garantida por uma abordagem de dupla camada combinando expressões regulares e validadores personalizados, eliminando dados sujos do banco de dados.


3. Fluxo de Dados do Pydantic V2

(1) Ciclo de Vida Completo

100%
flowchart TD
    A[JSON Request Body] --> B[model_validate]
    B --> C[Type Coercion]
    C --> D[field_validator]
    D --> E[model_validator]
    E --> F[Valid Model Instance]
    F --> G[model_dump]
    G --> H[JSON Response]
    F --> I[model_dump_json]
    I --> J[JSON String]
Fase Método Descrição
Análise de Entrada model_validate(data) Analisar e validar a partir de dict/JSON
Conversão de Tipo Automática "42"42, "9.99"9.99
Validação de Campo @field_validator Validação Personalizada para um Único Campo
Validação de Modelo @model_validator Validação Conjunta de Campos Cruzados
Serialização de Saída model_dump() / model_dump_json() Converter Modelo para dict/JSON

(1) ▶ Exemplo: Modelo Base do Pydantic V2

PYTHON
from pydantic import BaseModel, Field

class ProductCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    category: str = Field(max_length=100)
    base_price: float = Field(gt=0, description="Preço base em USD")

# Analisar e validar
data = {"name": "Widget", "category": "electronics", "base_price": 29.99}
product = ProductCreate.model_validate(data)
print(product.model_dump())

Saída:

TEXT
{'name': 'Widget', 'category': 'electronics', 'base_price': 29.99}

4. field_validator e model_validator

(1) Comparação de Migração V1 → V2

100%
flowchart LR
    V1[Pydantic V1] --> Migrate[V1 → V2 Migration]
    Migrate --> V2[Pydantic V2]
    V1 --- A["@validator('field')"]
    V2 --- B["@field_validator('field')"]
    V1 --- C["@root_validator"]
    V2 --- D["@model_validator(mode='after')"]
    V1 --- E["class Config:"]
    V2 --- F["model_config = ConfigDict(...)"]
    V1 --- G[".dict()"]
    V2 --- H[".model_dump()"]
Sintaxe V1 Sintaxe V2 Descrição
@validator("field") @field_validator("field") Validador de Campo
@root_validator @model_validator(mode="after") Validação em Nível de Modelo
class Config: orm_mode = True model_config = ConfigDict(from_attributes=True) Modelo ORM
.dict() .model_dump() Serializar para dict
.json() .model_dump_json() Serializar para JSON

(1) ▶ Exemplo: field_validator—Validação de Campo Único

PYTHON
from pydantic import BaseModel, Field, field_validator

class PriceCreate(BaseModel):
    product_id: int = Field(gt=0)
    price: float = Field(gt=0)
    currency: str = Field(default="USD", max_length=3)

    @field_validator("price")
    @classmethod
    def price_precision(cls, v: float) -> float:
        # Arredondar para 2 casas decimais
        return round(v, 2)

    @field_validator("currency")
    @classmethod
    def valid_currency(cls, v: str) -> str:
        allowed = {"USD", "EUR", "GBP", "JPY", "CNY"}
        if v not in allowed:
            raise ValueError(f"Currency must be one of {allowed}")
        return v.upper()

# Testar validação
p = PriceCreate(product_id=1, price=9.999, currency="usd")
print(p.model_dump())

Saída:

TEXT
{'product_id': 1, 'price': 10.0, 'currency': 'USD'}

(2) ▶ Exemplo: model_validator—validação de campos cruzados

PYTHON
from pydantic import BaseModel, Field, model_validator

class PriceRangeQuery(BaseModel):
    min_price: float = Field(ge=0, description="Preço mín em USD")
    max_price: float = Field(ge=0, description="Preço máx em USD")

    @model_validator(mode="after")
    def check_range(self):
        if self.min_price > self.max_price:
            raise ValueError("min_price must be <= max_price")
        return self

# Válido
valid = PriceRangeQuery(min_price=10, max_price=100)
print(valid.model_dump())

# Inválido - gera erro de validação
# PriceRangeQuery(min_price=100, max_price=10)

Saída:

TEXT
{'min_price': 10.0, 'max_price': 100.0}

5. Restrições Avançadas do Field() e JSON Schema

(1) Referência Rápida dos Parâmetros do Field()

Parâmetro Tipo Descrição Mapeamento JSON Schema
gt Valor Maior que exclusiveMinimum
ge Valor Maior ou igual a minimum
lt Valor Menor que exclusiveMaximum
le Valor Menor ou igual a maximum
min_length String Comprimento Mínimo minLength
max_length String Comprimento Máximo maxLength
pattern String Expressão Regular pattern
default Qualquer Padrão default
examples Lista Valores de Exemplo examples
description String Descrição description
alias String Alias do Campo Mapeamento de Alias

(1) ▶ Exemplo: Restrições do Field() e JSON Schema

PYTHON
from pydantic import BaseModel, Field

class ProductCreate(BaseModel):
    name: str = Field(
        min_length=1,
        max_length=200,
        description="Nome de exibição do produto",
        examples=["Wireless Mouse", "USB Cable"],
    )
    sku: str = Field(
        pattern=r"^[A-Z]{2}-\d{4,6}$",
        description="Código SKU: 2 letras + 4-6 dígitos",
        examples=["EL-1234", "CB-567890"],
    )
    base_price: float = Field(
        gt=0,
        le=999999.99,
        description="Preço base em USD",
        examples=[9.99, 49.99, 199.99],
    )

# Visualizar JSON Schema gerado
print(ProductCreate.model_json_schema())

Saída:

TEXT
# Execução Bem-sucedida

6. Modelos Aninhados e Combinações de Modelos

(1) Estrutura de Modelo Aninhado

(1) ▶ Exemplo: Modelo aninhado do PriceTracker

PYTHON
from pydantic import BaseModel, Field
from typing import Optional

class PriceInfo(BaseModel):
    amount: float = Field(gt=0, description="Valor do preço em USD")
    currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")
    source: str = Field(max_length=100, description="Fonte do preço")

class ProductCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    category: str = Field(max_length=100)
    current_price: PriceInfo  # Modelo aninhado
    original_price: Optional[PriceInfo] = None  # Aninhado opcional

# Validação aninhada
data = {
    "name": "Wireless Mouse",
    "category": "electronics",
    "current_price": {"amount": 29.99, "currency": "USD", "source": "Amazon"},
    "original_price": {"amount": 49.99, "currency": "USD", "source": "Amazon"},
}
product = ProductCreate.model_validate(data)
print(product.model_dump())

Saída:

TEXT
# Execução Bem-sucedida

(2) ▶ Exemplo: Union e Literal

PYTHON
from pydantic import BaseModel, Field
from typing import Union, Literal

class SinglePrice(BaseModel):
    type: Literal["single"] = "single"
    amount: float = Field(gt=0)

class RangePrice(BaseModel):
    type: Literal["range"] = "range"
    min_amount: float = Field(gt=0)
    max_amount: float = Field(gt=0)

class ProductPrice(BaseModel):
    product_id: int = Field(gt=0)
    pricing: Union[SinglePrice, RangePrice]  # União discriminada

# FastAPI usa o campo "type" para determinar qual modelo validar
data = {"product_id": 1, "pricing": {"type": "range", "min_amount": 10, "max_amount": 50}}
pp = ProductPrice.model_validate(data)
print(pp.model_dump())

Saída:

TEXT
# Execução Bem-sucedida

(2) Configuração ConfigDict

(3) ▶ Exemplo: model_config e o padrão ORM

PYTHON
from pydantic import BaseModel, ConfigDict

class ProductResponse(BaseModel):
    model_config = ConfigDict(
        from_attributes=True,  # Habilitar modo ORM (ler de objetos SQLAlchemy)
        populate_by_name=True,  # Permitir tanto nome do campo quanto alias
        json_schema_extra={
            "examples": [{"id": 1, "name": "Widget", "price": 9.99}]
        },
    )

    id: int
    name: str
    price: float

# Com from_attributes=True, pode criar a partir de atributos de objeto
class FakeORMObject:
    def __init__(self):
        self.id = 1
        self.name = "Widget"
        self.price = 9.99

orm_obj = FakeORMObject()
response = ProductResponse.model_validate(orm_obj)
print(response.model_dump())

Saída:

TEXT
{'id': 1, 'name': 'Widget', 'price': 9.99}

7. Exemplo Completo

A validação de campo e validação de campos cruzados do Pydantic V2, combinados com o padrão ORM e corpos de requisição do FastAPI, permitem um fluxo de trabalho completo de validação de entrada de dados.

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict

class PriceCreate(BaseModel):
    product_name: str = Field(min_length=1, max_length=100)
    price: float = Field(gt=0, description="O preço deve ser maior que 0")
    currency: str = "USD"

    @field_validator("currency")
    @classmethod
    def validate_currency(cls, v: str) -> str:
        if v not in ("USD", "EUR", "GBP"):
            raise ValueError("currency must be USD/EUR/GBP")
        return v

class PriceRangeQuery(BaseModel):
    min_price: float = Field(ge=0)
    max_price: float = Field(ge=0)

    @model_validator(mode="after")
    def validate_range(self):
        if self.min_price > self.max_price:
            raise ValueError("min_price must <= max_price")
        return self

app = FastAPI()

@app.post("/prices")
async def create_price(data: PriceCreate):
    return data.model_dump()

Saída:

TEXT
POST /prices {"product_name":"Widget","price":9.99} → {"product_name":"Widget","price":9.99,"currency":"USD"}
POST /prices {"product_name":"","price":-1} → 422 Validation Error

❓ Perguntas Frequentes

P Como escolher entre field_validator e model_validator?
R Use field_validator para validação de campo único (como verificações de formato), e use model_validator para validação de campos cruzados (como min_price <= max_price).
P A anotação @validator do V1 ainda pode ser usada?
R O V2 mantém uma camada de compatibilidade, mas emite um aviso de depreciação. Novos projetos devem usar @field_validator ou @model_validator, e projetos existentes devem migrar o mais rápido possível.
P O que from_attributes=True faz?
R Permite criar modelos Pydantic diretamente de objetos ORM (como instâncias de modelos SQLAlchemy), lendo os atributos do objeto em vez de um dicionário. Esta é uma configuração fundamental para FastAPI + SQLAlchemy.
P Qual é a diferença entre examples do Field() e json_schema_extra?
R examples é uma lista de valores de exemplo para um campo, mapeada para exemplos OpenAPI; json_schema_extra é uma propriedade de schema adicional em nível de modelo.
P Quais são os problemas com modelos aninhados muito profundos?
R Modelos aninhados com mais de 3 níveis aumentam a latência de validação e tornam a depuração mais difícil. Recomendamos achatar a estrutura ou decompô-la usando um padrão composto.
P Quanto melhor é o desempenho do Pydantic V2 em comparação ao V1?
R A validação central é 5-50 vezes mais rápida (implementação em Rust), e a serialização é 2-10 vezes mais rápida. Há uma melhoria notável em cenários envolvendo milhões de pontos de dados.

📖 Resumo


📝 Exercícios

  1. Problema Básico (Dificuldade ⭐): Crie o modelo PriceCreate, que inclui product_id: int (> 0) e price: float (> 0), e verifique que entrada inválida aciona um erro de validação. Dica: BaseModel + Field(gt=0)
  2. Exercício Avançado (Dificuldade ⭐⭐): Adicione ao PriceCreate um @field_validator para validar que o campo currency só pode conter USD/EUR/GBP, e adicione @model_validator para garantir min_price <= max_price. Dica: field_validator + model_validator(mode="after")
  3. Desafio (Dificuldade ⭐⭐⭐): Design um sistema de modelo aninhado completo para o PriceTracker: ProductCreate contém PriceInfo (amount + currency); a currency em PriceInfo é restringida por uma expressão regular; e o SKU em ProductCreate é restringido pelo formato em pattern. Dica: Field(pattern=...) + BaseModel aninhado

---|

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%