Validador de Esquemas JSON com Python Puro: Seu Firewall de Dados Sem Dependências
Você já teve um script quebrar no meio da noite porque uma API mudou o formato da resposta? Eu sim. Três vezes na mesma semana. A terceira vez, às 3 da manhã, eu jurei: nunca mais confio em dados externos sem validar.
O problema não é a API. É a suposição de que estrutura de dados é um contrato que todo mundo respeita. Spoiler: não é. APIs mudam silenciosamente. Bancos legados retornam null onde deveria ter número. CSVs de terceiros têm colunas a mais no dia do deploy.
A solução? Um validador de esquemas JSON leve, em Python puro, que você cola no projeto e esquece. Sem instalar jsonschema, sem esperar releases de dependência, sem vazar dados para validadores em nuvem.
O Problema: Por Que json.loads Não É Suficiente
O erro clássico:
import requests
def process_order(data):
total = data['items'][0]['price'] * data['items'][0]['quantity']
return {'total': total}
# Funciona perfeitamente... até não funcionar
response = requests.get('https://api.example.com/order/123')
order = process_order(response.json())
O que pode dar errado? Tudo.
itemspode sernullem vez de listapricepode vir como string"19.99"em vez de19.99quantitypode ser negativo (ou zero)- A chave
itemspode não existir items[0]pode não ter a chaveprice
Cada um desses cenários é um KeyError, TypeError ou — pior — um cálculo silenciosamente errado que corrompe seu banco de dados.
A Solução: Validador Declarativo em Python Puro
O que você precisa não é complexidade. É um contrato que falha rápido e falha claro.
"""
json_guard.py — Validador de esquemas JSON leve, sem dependências.
Uso: validate(data, schema) → levanta ValidationError detalhada.
"""
from typing import Any, Callable, Union
class ValidationError(Exception):
"""Erro de validação com caminho completo até o campo problemático."""
def __init__(self, message: str, path: str = ""):
self.path = path
super().__init__(f"[{path}] {message}" if path else message)
# ---- Tipos primitivos ----
def String(min_len: int = 0, max_len: int = None) -> Callable:
def check(value: Any, path: str = "") -> str:
if not isinstance(value, str):
raise ValidationError(f"expected string, got {type(value).__name__}", path)
if len(value) < min_len:
raise ValidationError(f"string too short ({len(value)} < {min_len})", path)
if max_len is not None and len(value) > max_len:
raise ValidationError(f"string too long ({len(value)} > {max_len})", path)
return value
return check
def Integer(min_val: int = None, max_val: int = None) -> Callable:
def check(value: Any, path: str = "") -> int:
if not isinstance(value, int) or isinstance(value, bool):
raise ValidationError(f"expected int, got {type(value).__name__}", path)
if min_val is not None and value < min_val:
raise ValidationError(f"too small ({value} < {min_val})", path)
if max_val is not None and value > max_val:
raise ValidationError(f"too large ({value} > {max_val})", path)
return value
return check
def Float(min_val: float = None, max_val: float = None) -> Callable:
def check(value: Any, path: str = "") -> float:
if not isinstance(value, (int, float)) or isinstance(value, bool):
raise ValidationError(f"expected float, got {type(value).__name__}", path)
v = float(value)
if min_val is not None and v < min_val:
raise ValidationError(f"too small ({v} < {min_val})", path)
if max_val is not None and v > max_val:
raise ValidationError(f"too large ({v} > {max_val})", path)
return v
return check
def Boolean() -> Callable:
def check(value: Any, path: str = "") -> bool:
if not isinstance(value, bool):
raise ValidationError(f"expected bool, got {type(value).__name__}", path)
return value
return check
def List(item_schema: Callable, min_len: int = 0, max_len: int = None) -> Callable:
def check(value: Any, path: str = "") -> list:
if not isinstance(value, list):
raise ValidationError(f"expected list, got {type(value).__name__}", path)
if len(value) < min_len:
raise ValidationError(f"list too short ({len(value)} < {min_len})", path)
if max_len is not None and len(value) > max_len:
raise ValidationError(f"list too long ({len(value)} > {max_len})", path)
return [item_schema(v, f"{path}[{i}]") for i, v in enumerate(value)]
return check
def Dict(schema: dict, allow_extra: bool = False) -> Callable:
def check(value: Any, path: str = "") -> dict:
if not isinstance(value, dict):
raise ValidationError(f"expected dict, got {type(value).__name__}", path)
result = {}
for key, field_schema in schema.items():
if key not in value:
raise ValidationError(f"missing required key '{key}'", path)
result[key] = field_schema(value[key], f"{path}.{key}" if path else key)
if not allow_extra:
extras = set(value.keys()) - set(schema.keys())
if extras:
raise ValidationError(f"unexpected keys: {extras}", path)
return result
return check
def Optional(schema: Callable, default: Any = None) -> Callable:
def check(value: Any, path: str = "") -> Any:
if value is None:
return default
return schema(value, path)
return check
def Union(*schemas: Callable) -> Callable:
def check(value: Any, path: str = "") -> Any:
for schema in schemas:
try:
return schema(value, path)
except ValidationError:
pass
raise ValidationError(f"no union variant matched", path)
return check
def Literal(*values: Any) -> Callable:
def check(value: Any, path: str = "") -> Any:
if value not in values:
raise ValidationError(f"expected one of {values}, got {value!r}", path)
return value
return check
def validate(data: Any, schema: Callable) -> Any:
"""Valida dados contra um esquema. Retorna dados limpos ou levanta ValidationError."""
return schema(data, "")
Usando na Prática: O Pedido que Nunca Mais Quebra
from json_guard import (
String, Integer, Float, Boolean,
List, Dict, Optional, validate, ValidationError
)
# ---- Esquema do pedido ----
ItemSchema = Dict({
"product_id": String(min_len=1),
"name": String(min_len=1, max_len=200),
"price": Float(min_val=0),
"quantity": Integer(min_val=1),
"discount": Optional(Float(min_val=0, max_val=1), default=0.0),
})
OrderSchema = Dict({
"order_id": String(min_len=1),
"customer_email": String(min_len=5),
"items": List(ItemSchema, min_len=1),
"status": Literal("pending", "paid", "shipped", "cancelled"),
"notes": Optional(String(), default=""),
})
# ---- Uso ----
raw_data = {
"order_id": "ORD-2024-001",
"customer_email": "cliente@exemplo.com",
"items": [
{"product_id": "SKU-42", "name": "Teclado Mecânico", "price": 299.90, "quantity": 1},
{"product_id": "SKU-7", "name": "Mouse", "price": 149.50, "quantity": 2, "discount": 0.1},
],
"status": "paid",
}
try:
clean_order = validate(raw_data, OrderSchema)
total = sum(
item["price"] * item["quantity"] * (1 - item["discount"])
for item in clean_order["items"]
)
print(f"Pedido válido. Total: R$ {total:.2f}")
except ValidationError as e:
print(f"Dados inválidos: {e}")
# Log, alerta, reprocessa — mas NÃO continua com dados sujos
Validação de Respostas de API: Seu Escudo Contra Mudanças Silenciosas
import requests
from json_guard import String, Integer, Dict, List, validate, ValidationError
ApiResponseSchema = Dict({
"success": bool,
"data": Dict({
"users": List(Dict({
"id": Integer(min_val=1),
"name": String(min_len=1),
"email": String(min_len=5),
})),
"total": Integer(min_val=0),
}),
"error": String(), # vazio em sucesso
})
def fetch_users():
response = requests.get("https://api.exemplo.com/users")
response.raise_for_status()
try:
validated = validate(response.json(), ApiResponseSchema)
return validated["data"]["users"]
except ValidationError as e:
# A API mudou! Alerta imediato, não crash silencioso.
send_alert(f"API contract violated: {e}")
raise
Validação de Configuração: Evite Deploy Quebrado
from json_guard import String, Integer, Boolean, Dict, List, Optional, Literal, validate
import json
ConfigSchema = Dict({
"database": Dict({
"host": String(min_len=1),
"port": Integer(min_val=1, max_val=65535),
"name": String(min_len=1),
"ssl": Boolean(),
}),
"workers": Integer(min_val=1, max_val=32),
"features": Dict({
"caching": Boolean(),
"rate_limit": Optional(Boolean(), default=False),
}),
"log_level": Literal("DEBUG", "INFO", "WARNING", "ERROR"),
})
with open("config.json") as f:
raw = json.load(f)
config = validate(raw, ConfigSchema)
# Se chegou aqui, a configuração está garantida
Testes Automáticos: Garanta que Validação Não Regreda
import pytest
from json_guard import String, Integer, Dict, List, Optional, validate, ValidationError
UserSchema = Dict({
"name": String(min_len=1),
"age": Integer(min_val=0, max_val=150),
"email": Optional(String()),
})
def test_valid_user():
data = {"name": "Ana", "age": 30}
result = validate(data, UserSchema)
assert result["name"] == "Ana"
assert result["age"] == 30
assert result["email"] is None # default do Optional
def test_invalid_age():
data = {"name": "Ana", "age": -5}
with pytest.raises(ValidationError) as exc:
validate(data, UserSchema)
assert "too small" in str(exc.value)
def test_missing_name():
data = {"age": 30}
with pytest.raises(ValidationError) as exc:
validate(data, UserSchema)
assert "missing required key 'name'" in str(exc.value)
Por Que Não Usar jsonschema ou pydantic?
| Critério | json_guard (este) | jsonschema | pydantic |
|---|---|---|---|
| Dependências | Zero | Sim (biblioteca) | Sim (pesada) |
| Tempo de import | ~1ms | ~50ms | ~200ms |
| Tamanho | ~150 linhas | ~3MB | ~20MB+ |
| Mensagens de erro | Caminho completo | Boa | Boa |
| Performance | Nativo Python | Rápido | Muito rápido (Rust) |
| Customização | Total | Limitada | Média |
Use este validador quando:
- Você está em ambiente restrito (sem pip, sem internet)
- O startup time importa (scripts CLI, lambdas frias)
- Você não quer confiar em mais uma dependência
- Precisa de validação customizada que bibliotecas não cobrem
Use Pydantic quando:
- Performance é crítica (milhões de validações/segundo)
- Você precisa de serialização automática
- Está em projeto grande com time acostumado
Extensões: Onde Ir Daqui
O código base é intencionalmente minimalista. Aqui está como estender:
# Validador customizado: CPF brasileiro
def CPF() -> Callable:
def check(value: Any, path: str = "") -> str:
s = String(min_len=11, max_len=14)(value, path)
s = s.replace(".", "").replace("-", "")
if len(s) != 11 or not s.isdigit():
raise ValidationError("invalid CPF format", path)
# ... lógica de dígitos verificadores
return s
return check
# Validador de regex
def Regex(pattern: str) -> Callable:
import re
compiled = re.compile(pattern)
def check(value: Any, path: str = "") -> str:
s = String()(value, path)
if not compiled.match(s):
raise ValidationError(f"does not match pattern {pattern!r}", path)
return s
return check
# Uso
EmailSchema = Regex(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
Conclusão
Dados externos são inimigos silenciosos. APIs mudam. CSVs corrompem. JSONs retornam null no lugar de lista. A única defesa é falhar rápido e falha claro.
Este validador de ~150 linhas é seu escudo. Cole no projeto, defina seus esquemas, e durma tranquilo sabendo que dados inválidos nunca mais chegarão na lógica de negócio.
O código completo está acima. Copie, adapte, e proteja suas automações.
O que você valida hoje que estava confiando cegamente ontem?
