Skip to content

操作指南:嵌套 JSON 校验

FT 参考:FT322(NENE2-FT/nestedlog)——带嵌套 items 校验的订单 API,items.N.field 错误路径,单次响应返回多个错误,错误代码,总额计算,19 个测试 / 43 个断言全部通过。

本指南展示如何校验嵌套 JSON 数组(例如订单行项目),并返回精确标识哪个嵌套字段失败的结构化错误路径。

数据库结构

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

端点

方法路径描述
POST/orders创建带 items 的订单
GET/orders列出订单
GET/orders/{id}获取带 items 的订单

创建订单

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
}

嵌套错误路径——items.N.field

每个 item 错误在字段路径中包含数组索引:

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

单次响应返回所有错误

所有校验失败——包括顶层和嵌套的——都被收集并一起返回。对于批量提交,绝不一次只返回一个错误:

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

校验规则

字段规则
customer必填,非空,最多 200 字符
items必填,非空数组
items[].product_id整数,≥ 1
items[].quantity整数,≥ 1
items[].unit_price数字(int 或 float),> 0

实现模式

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

        // 顶层校验
        $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;  // 无法进一步校验 items
        }

        // 带索引的嵌套 item 校验
        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;
    }
}

错误代码

代码含义
required字段缺失或为空
max-length超过最大长度
min-value低于最小值(int/float)
invalid-type类型错误(例如期望 int 但收到 string)

反模式

反模式风险
只返回第一个错误客户端必须提交、获取错误、修复、再次提交 N 次——对批量表单来说是极差的用户体验
嵌套 item 用平坦错误路径 "product_id"客户端无法确定哪个 item(索引 0、1、...)失败了
静默接受 unit_price: 0零价格 item 会破坏订单总额
只在顶层通过后才校验 items延迟反馈;应该在一次扫描中收集所有错误
遇到第一个 item 错误就停止校验掩盖剩余 item 中的进一步错误

基于 MIT 许可证发布。