Validador JSON Python Shield

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.

  • items pode ser null em vez de lista
  • price pode vir como string "19.99" em vez de 19.99
  • quantity pode ser negativo (ou zero)
  • A chave items pode não existir
  • items[0] pode não ter a chave price

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?

Posts Similares