Ir para o conteúdo

Campos validados (tipos prontos)

Em vez de repetir Field(gt=0, ...) em cada schema, o SDK traz tipos Annotated com a regra de validação embutida — o campo passa a se autodescrever. Seguem a mesma convenção *Field dos campos BR (CPFField, UFField, ...): tudo que termina em Field é um tipo de campo pronto pra schema. Valor inválido vira ValidationError → HTTP 422 pelo handler do SDK.

from tempest_fastapi_sdk import BaseSchema
from tempest_fastapi_sdk.utils import CentsField, PercentField, SlugField


class ProductCreateSchema(BaseSchema):
    slug: SlugField              # kebab-case minúsculo
    price_cents: CentsField      # int >= 0 (dinheiro em centavos)
    discount: PercentField       # float em [0, 100]

Inteiros

Tipo Regra Uso
PositiveIntField > 0 quantidades, contagens, ids 1-based
NonNegativeIntField >= 0 contadores que zeram
CentsField >= 0 dinheiro em unidades menores (centavos)
PortField 1..65535 porta TCP/UDP

Dinheiro em centavos

Guarde valores monetários como inteiro de centavos (CentsField) pra fugir do arredondamento de float binário. Divida por 100 só na borda de apresentação. Quando o contrato exige decimal exato (ex.: payload de gateway), use PriceField.

Floats

Tipo Regra
PositiveFloatField > 0
NonNegativeFloatField >= 0
PercentField 0..100
RatioField 0..1 (frações/probabilidades)
LatitudeField -90..90
LongitudeField -180..180

Decimais

PriceField é um Decimal não-negativo com 2 casas:

from tempest_fastapi_sdk import BaseSchema
from tempest_fastapi_sdk.utils import PriceField


class InvoiceSchema(BaseSchema):
    total: PriceField     # "9.99" -> Decimal("9.99"); "1.999" e "-1" rejeitados

Para razões e percentuais, existe o par decimal de RatioField / PercentField:

Tipo Regra Quando
DecimalRatioField Decimal em 0..1 Fração que multiplica dinheiro
DecimalPercentField Decimal em 0..100 Percentual que multiplica dinheiro
SignedDecimalRatioField Decimal <= 1 Fração derivada que pode ficar negativa
from decimal import Decimal

from tempest_fastapi_sdk import BaseSchema
from tempest_fastapi_sdk.utils import DecimalRatioField, PriceField


class QuoteSchema(BaseSchema):
    reference: PriceField
    discount: DecimalRatioField    # Decimal("0.28"), não 0.28

    def proposal(self) -> Decimal:
        """Return the reference amount minus the discount."""
        return self.reference * (Decimal(1) - self.discount)

Não use RatioField quando a fração multiplica dinheiro

RatioField e PercentField são anotados em float. Decimal("0.28") vira 0.28, o Pydantic aceita calado, e a primeira multiplicação por um PriceField levanta TypeError — ou pior, alguém "conserta" com um float() e o centavo se perde. Em orçamento de licitação, um centavo desclassifica proposta.

Por que SignedDecimalRatioField tem teto mas não piso

Ele é para uma fração derivada: a linha de saldo que as premissas não deixaram folga, a diferença de fechamento que o documento precisa expor em vez de zerar. Acima de 100% é bug; abaixo de 0% é notícia.

Strings

Tipo Regra
NonEmptyStrField apara espaços e rejeita vazio (só-espaço também)
SlugField kebab-case minúsculo (meu-post-1)
HexColorField cor hex CSS (#fff ou #abc123)
from tempest_fastapi_sdk import BaseSchema
from tempest_fastapi_sdk.utils import HexColorField, NonEmptyStrField, SlugField


class CategorySchema(BaseSchema):
    name: NonEmptyStrField     # "  Bebidas " -> "Bebidas"; "   " rejeitado
    slug: SlugField
    color: HexColorField

Locale

LocaleField é o par do UFField para idioma: um Annotated[Locale, ...] que normaliza a entrada para a forma canônica do enum Locale. Sob BaseSchema o atributo populado é a str canônica, não o membro — BaseSchema liga use_enum_values=True, então "pt_BR" entra e "pt-BR" sai, e isinstance(valor, Locale) é False. Num pydantic.BaseModel puro, sem essa config, você recebe o membro. Aceita variação de caixa/separador ("pt_BR", "PT-BR") e o subtag primário ("pt"Locale.PT_BR); uma tag fora do enum vira 422.

from tempest_fastapi_sdk import BaseSchema, Locale
from tempest_fastapi_sdk.utils import LocaleField


class ProfileUpdateSchema(BaseSchema):
    locale: LocaleField | None = None   # "pt_BR" -> Locale.PT_BR; "xx-YY" -> 422

Enum vs. Field

Use o enum Locale quando você já tem a tag canônica (constantes, lógica interna). Use o LocaleField num schema de request, quando a entrada é do cliente e vale normalizar — mesma relação de UF para UFField. Para gravar o idioma numa tabela, veja o LocaleColumnMixin.

Chave Pix

PixKeyField valida os cinco tipos de chave Pix do BACEN num campo só e normaliza pra forma canônica. Chave inválida → ValidationError → 422.

Tipo Exemplo Normaliza pra
CPF 529.982.247-25 52998224725 (dígitos)
CNPJ 11.222.333/0001-81 11222333000181 (dígitos)
E-mail Ana@Example.com ana@example.com (minúsculo)
Telefone +5511999998888 +5511999998888 (E.164)
Aleatória (EVP) 123e4567-e89b-12d3-a456-426614174000 mesmo, minúsculo (UUID)
from tempest_fastapi_sdk import BaseSchema, CentsField
from tempest_fastapi_sdk.utils import PixKeyField


class PixTransferSchema(BaseSchema):
    to_key: PixKeyField      # aceita CPF/CNPJ/e-mail/telefone/aleatória
    amount_cents: CentsField

Precisa saber qual tipo a chave é (roteamento, UI, regra de negócio)? Use os helpers — não exigem o campo:

from tempest_fastapi_sdk.utils import PixKeyType, detect_pix_key_type, is_valid_pix_key


detect_pix_key_type("ana@example.com")   # -> PixKeyType.EMAIL
detect_pix_key_type("+5511999998888")    # -> PixKeyType.PHONE
detect_pix_key_type("chave-invalida")    # -> None
is_valid_pix_key("529.982.247-25")       # -> True

Validação por forma + dígitos

A detecção é por formato: @ → e-mail, + inicial → telefone E.164, UUID → aleatória; senão, dígitos + dígitos verificadores decidem entre CPF e CNPJ (um CPF com DV errado é rejeitado). O telefone segue E.164 brasileiro (+55 + DDD + número, 10–11 dígitos após o +55).

Exemplo completo (schema + rota + 422)

Os tipos são Annotated puros — funcionam em qualquer BaseSchema e o FastAPI já converte o ValidationError em 422 pelo handler do SDK. Programa completo:

# src/schemas/product.py
from tempest_fastapi_sdk import BaseSchema
from tempest_fastapi_sdk.utils import (
    CentsField,
    NonEmptyStrField,
    PercentField,
    SlugField,
)


class ProductCreateSchema(BaseSchema):
    name: NonEmptyStrField
    slug: SlugField
    price_cents: CentsField
    discount_percent: PercentField = 0.0
# src/api/routers/products.py
from fastapi import APIRouter

from src.schemas.product import ProductCreateSchema

router = APIRouter()


@router.post("/products")
async def create_product(payload: ProductCreateSchema) -> dict[str, str]:
    """Cria um produto. Payload inválido nunca chega aqui — vira 422."""
    return {"slug": payload.slug}

Um POST inválido (price_cents: -5, slug: "Não Slug") recebe 422 com o corpo que o handler do SDK padroniza:

{
  "detail": [
    {"loc": ["body", "price_cents"], "msg": "Input should be greater than or equal to 0"},
    {"loc": ["body", "slug"], "msg": "String should match pattern ..."}
  ]
}

Opcional, default e lista

Compõem como qualquer tipo Pydantic: CentsField | None = None, discount: PercentField = 0.0, list[SlugField]. A regra continua valendo em cada elemento da lista.

Compondo seus próprios campos

Todo *Field é só um Annotated[tipo, Field(...)]. Precisa de uma regra que o SDK não traz? Monte a sua com o mesmo padrão — nada de mágica:

from typing import Annotated

from pydantic import Field

from tempest_fastapi_sdk import BaseSchema


# Quantidade em estoque: inteiro de 0 a 100000.
StockField = Annotated[int, Field(ge=0, le=100_000)]

# Sigla de 3 letras maiúsculas (ex.: moeda ISO-4217).
CurrencyCodeField = Annotated[str, Field(pattern=r"^[A-Z]{3}$")]


class SkuSchema(BaseSchema):
    stock: StockField
    currency: CurrencyCodeField      # "BRL" ok; "brl" / "REAL" rejeitados

Prefira os tipos do SDK quando existir um equivalente (autodescrição + consistência entre serviços); crie os seus só pras regras específicas do domínio.

Pegadinhas comuns

  • CentsField vs PriceField — guarde dinheiro como inteiro de centavos (CentsField) pra fugir do float binário; use PriceField (Decimal, 2 casas) só quando o contrato exige decimal.
  • PercentField vs RatioFieldPercent é 0..100 (o humano digita "15"); Ratio é 0..1 (fração/probabilidade, "0.15"). Não misture os dois no mesmo fluxo.
  • NonEmptyStrField apara espaços antes de validar — " " (só espaço) é rejeitado como vazio.

Recap

  • Tudo Annotated[..., regra], com sufixo *Field — autodescritivo, sem repetir Field(...) no schema.
  • Inteiros: PositiveIntField, NonNegativeIntField, CentsField, PortField.
  • Floats: PositiveFloatField, NonNegativeFloatField, PercentField, RatioField, LatitudeField, LongitudeField.
  • Decimal: PriceField (2 casas, >= 0).
  • Strings: NonEmptyStrField, SlugField, HexColorField.
  • BR: CPFField, CNPJField, CPFOrCNPJField, PhoneBRField, CEPField, UFField, CityNameField (veja Helpers brasileiros).
  • Pix: PixKeyField (CPF/CNPJ/e-mail/telefone/aleatória) + detect_pix_key_type / is_valid_pix_key.
  • Dinheiro: prefira CentsField (inteiro) pra guardar; PriceField no contrato decimal.
  • Componha os seus com Annotated[tipo, Field(...)] pras regras do domínio.