Skip to content

Como Fazer: Validação de JSON Aninhado

Referência FT: FT322 (NENE2-FT/nestedlog) — API de pedidos com validação de itens aninhados, caminhos de erro items.N.field, múltiplos erros em uma única resposta, códigos de erro, cálculo de total, 19 testes / 43 asserções PASS.

Este guia mostra como validar arrays JSON aninhados (por ex., itens de linha de pedido) e retornar caminhos de erro estruturados que identificam exatamente qual campo aninhado falhou.

Schema

sql
CREATE TABLE orders (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    customer   TEXT    NOT NULL,
    total      REAL    NOT NULL DEFAULT 0.0,
    created_at TEXT    NOT NULL
);

CREATE TABLE order_items (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    order_id   INTEGER NOT NULL REFERENCES orders(id),
    product_id INTEGER NOT NULL,
    quantity   INTEGER NOT NULL,
    unit_price REAL    NOT NULL
);

Endpoints

MétodoCaminhoDescrição
POST/ordersCriar pedido com itens
GET/ordersListar pedidos
GET/orders/{id}Obter pedido com itens

Criar Pedido

php
POST /orders
{
  "customer": "Alice",
  "items": [
    {"product_id": 1, "quantity": 2, "unit_price": 9.99},
    {"product_id": 2, "quantity": 1, "unit_price": 4.50}
  ]
}
 201
{
  "id": 1,
  "customer": "Alice",
  "items": [...],
  "total": 24.48      // 2×9.99 + 1×4.50
}

Caminhos de Erro Aninhados — items.N.field

Cada erro de item inclui o índice do array no caminho do campo:

php
POST /orders
{
  "customer": "Alice",
  "items": [
    {"product_id": "not-an-int", "quantity": 2, "unit_price": 9.99},
    {"product_id": 1, "quantity": 1, "unit_price": -5.0}
  ]
}
 422
{
  "errors": [
    {"field": "items.0.product_id", "message": "...", "code": "invalid-type"},
    {"field": "items.1.unit_price",  "message": "...", "code": "min-value"}
  ]
}

Todos os Erros em Uma Resposta

Todas as falhas de validação — tanto no nível superior quanto aninhadas — são coletadas e retornadas juntas. Nunca retorne um erro por vez para submissões em lote:

php
POST /orders
{
  "customer": "",      // erro: obrigatório
  "items": [
    {"product_id": 0, "quantity": -1, "unit_price": 1.0}  // 2 erros
  ]
}
 422
{
  "errors": [
    {"field": "customer",          "code": "required"},
    {"field": "items.0.product_id","code": "min-value"},
    {"field": "items.0.quantity",  "code": "min-value"}
  ]
}

Regras de Validação

CampoRegra
customerObrigatório, não-vazio, máx 200 chars
itemsObrigatório, array não-vazio
items[].product_idInteiro, ≥ 1
items[].quantityInteiro, ≥ 1
items[].unit_priceNúmero (int ou float), > 0

Padrão de Implementação

php
final class OrderValidator
{
    /** @return list<ValidationError> */
    public function validate(array $data): array
    {
        $errors = [];

        // Validação no nível superior
        $customer = trim($data['customer'] ?? '');
        if ($customer === '') {
            $errors[] = new ValidationError('customer', 'required', 'required');
        } elseif (strlen($customer) > 200) {
            $errors[] = new ValidationError('customer', 'max 200 chars', 'max-length');
        }

        $items = $data['items'] ?? null;
        if (!is_array($items) || $items === []) {
            $errors[] = new ValidationError('items', 'required non-empty array', 'required');
            return $errors;  // não pode validar itens mais
        }

        // Validação de item aninhado com índice
        foreach ($items as $i => $item) {
            $prefix = "items.{$i}";

            $productId = $item['product_id'] ?? null;
            if (!is_int($productId) || $productId < 1) {
                $errors[] = new ValidationError("{$prefix}.product_id", 'must be int >= 1', 'min-value');
            }

            $quantity = $item['quantity'] ?? null;
            if (!is_int($quantity) || $quantity < 1) {
                $errors[] = new ValidationError("{$prefix}.quantity", 'must be int >= 1', 'min-value');
            }

            $price = $item['unit_price'] ?? null;
            if ((!is_int($price) && !is_float($price)) || $price <= 0) {
                $errors[] = new ValidationError("{$prefix}.unit_price", 'must be number > 0', 'min-value');
            }
        }

        return $errors;
    }
}

Códigos de Erro

CódigoSignificado
requiredCampo ausente ou vazio
max-lengthExcede o comprimento máximo
min-valueAbaixo do valor mínimo (int/float)
invalid-typeTipo errado (por ex. string onde int era esperado)

O Que NÃO Fazer

Anti-padrãoRisco
Retornar apenas o primeiro erroCliente deve submeter, receber erro, corrigir, resubmeter N vezes — UX terrível para formulários em lote
Caminho de erro plano "product_id" para itens aninhadosCliente não consegue identificar qual item (índice 0, 1, ...) falhou
Aceitar unit_price: 0 silenciosamenteItens com preço zero corrompem totais do pedido
Validar itens somente após o nível superior passarAtrasa o feedback; colete todos os erros em uma única passagem
Parar validação no primeiro erro de itemMascarar outros erros nos itens restantes

Publicado sob a licença MIT.