Skip to content

How-to: Verschachtelte JSON-Validierung

FT-Referenz: FT322 (NENE2-FT/nestedlog) — Bestellungs-API mit verschachtelter Artikel-Validierung, items.N.field-Fehlerpfade, Multi-Fehler-Einzelantwort, Fehlercodes, Gesamtberechnung, 19 Tests / 43 Assertions PASS.

Diese Anleitung zeigt, wie verschachtelte JSON-Arrays (z.B. Bestellpositions-Artikel) validiert und strukturierte Fehlerpfade zurückgegeben werden, die genau angeben, welches verschachtelte Feld fehlgeschlagen ist.

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
);

Endpunkte

MethodePfadBeschreibung
POST/ordersBestellung mit Artikeln erstellen
GET/ordersBestellungen auflisten
GET/orders/{id}Bestellung mit Artikeln abrufen

Bestellung erstellen

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
}

Verschachtelte Fehlerpfade — items.N.field

Jeder Artikelfehler enthält den Array-Index im Feldpfad:

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"}
  ]
}

Alle Fehler in einer Antwort

Alle Validierungsfehler — sowohl auf oberster Ebene als auch verschachtelt — werden gesammelt und zusammen zurückgegeben. Bei Batch-Übermittlungen niemals einen Fehler nach dem anderen zurückgeben:

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

Validierungsregeln

FeldRegel
customerPflichtfeld, nicht leer, max 200 Zeichen
itemsPflichtfeld, nicht-leeres Array
items[].product_idInteger, ≥ 1
items[].quantityInteger, ≥ 1
items[].unit_priceZahl (int oder float), > 0

Implementierungsmuster

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

        // Validierung auf oberster Ebene
        $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;  // Artikel können nicht weiter validiert werden
        }

        // Verschachtelte Artikel-Validierung mit Index
        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;
    }
}

Fehlercodes

CodeBedeutung
requiredFeld fehlt oder ist leer
max-lengthÜberschreitet maximale Länge
min-valueUnterhalb des Minimalwerts (int/float)
invalid-typeFalscher Typ (z.B. String, wo int erwartet wird)

Was man NICHT tun sollte

Anti-MusterRisiko
Nur ersten Fehler zurückgebenClient muss N-mal einreichen, Fehler bekommen, korrigieren, erneut einreichen — schreckliche UX für Batch-Formulare
Flacher Fehlerpfad "product_id" für verschachtelte ArtikelClient kann nicht identifizieren, welcher Artikel (Index 0, 1, ...) fehlgeschlagen ist
unit_price: 0 stillschweigend akzeptierenNullpreisartikel korrumpieren Bestellsummen
Artikel nur nach bestandener Top-Level-Validierung validierenVerzögert Feedback; alle Fehler in einem Durchgang sammeln
Validierung beim ersten Artikelfehler stoppenVerbirgt weitere Fehler in verbleibenden Artikeln

Veröffentlicht unter der MIT-Lizenz.