Claude 工作流实验室
独立第三方实践站 · 非 Anthropic 官方;域名中的 6 不代表官方模型版本独立中文指南
代码理解

用 Claude 给 JSON 接口补运行时校验:别把类型声明当保证

通过一个商品接口,把未知 JSON 变为可检查的数据,区分 HTTP 错误、解析错误和字段错误。

页面类型检查通过,上线后却出现“价格是字符串”或“库存字段消失”。常见原因是把接口响应直接断言成某个类型。类型声明描述的是代码中的约定,外部服务、旧缓存和测试替身仍可能返回不符合约定的数据。让 Claude 帮忙时,应该把任务放在系统边界:收到什么、接受什么、拒绝什么,以及失败后界面怎样恢复。

一 先给出成功与失败样本

下面假设商品接口的有效响应是 {"id":"p1","priceCents":1990,"inStock":true}。金额使用非负整数分,商品编号不能为空,库存状态必须是布尔值。未知字段允许出现,但不会被复制到返回值。故意提供 priceCents: "1990"、inStock: "false" 和 null,防止 Claude 用 Number() 或 Boolean() 把坏数据悄悄变成看似正确的值。

如果你使用 TypeScript,入口可声明为 unknown,在检查后缩小类型;TypeScript narrowing 文档解释了这种检查与类型收窄的关系。本文使用普通 JavaScript,便于独立运行。

二 建一个严格但有限的边界函数

保存为 product.mjs,运行 node product.mjs:

js 示例代码
import assert from 'node:assert/strict';

function parseProduct(value) {
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
    throw new TypeError('商品必须是对象');
  }
  if (typeof value.id !== 'string' || value.id.trim() === '') {
    throw new TypeError('id 必须是非空字符串');
  }
  if (!Number.isSafeInteger(value.priceCents) || value.priceCents < 0) {
    throw new TypeError('priceCents 必须是非负整数分');
  }
  if (typeof value.inStock !== 'boolean') {
    throw new TypeError('inStock 必须是布尔值');
  }
  return { id: value.id, priceCents: value.priceCents, inStock: value.inStock };
}

async function loadProduct(url, fetcher = fetch) {
  const response = await fetcher(url);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return parseProduct(await response.json());
}

const good = { id: 'p1', priceCents: 1990, inStock: true };
assert.deepEqual(parseProduct({ ...good, note: 'ignored' }), good);
for (const bad of [null, [], { ...good, priceCents: '1990' },
                   { ...good, inStock: 'false' }]) {
  assert.throws(() => parseProduct(bad));
}
assert.deepEqual(await loadProduct('/products/p1', async () => ({
  ok: true, json: async () => good
})), good);
await assert.rejects(() => loadProduct('/products/p1', async () => ({
  ok: false, status: 503
})), /HTTP 503/);
console.log('product boundary checks passed');

fetcher 参数让测试能够替换网络,而不需要真实请求。它并不模拟浏览器的全部行为。MDN 的 Fetch 使用说明提醒:HTTP 错误状态本身不一定让 fetch() 拒绝,因此需要检查 response.ok。

三 可复制提示词

可复制提示词
请审查我的商品接口边界。成功结构:id 为非空字符串,priceCents 为非负安全整数,
inStock 为布尔值;未知字段忽略。这里是脱敏响应样本和现有读取函数:[粘贴]。
请分别处理 HTTP 非成功、JSON 解析失败、字段不合法,不要自动把字符串转成数字或布尔值。
给出不新增依赖的最小修复,以及可注入 fetch 替身的测试。
输出接口假设、完整代码、验证命令、仍未覆盖的风险;不要把未执行测试写成通过。

如果字段允许缺省,要进一步说明缺省的业务含义。例如缺少库存字段是否代表“未知”,并不自动等于“无货”。让模型列出疑问,再由你决定规则。

四 接入与验收

在页面入口捕获错误,展示可理解的失败状态,并提供适合该页面的重试入口。不要把原始响应、令牌或用户数据直接打印给终端用户。请求超时、认证和重试策略属于下一层设计,不能靠字段校验解决。

本文示例已在 Node.js 24.19.0 中执行;验证覆盖正常响应、类型错误、额外字段和模拟的 503 状态,没有访问生产接口。项目验收还应补非法 JSON、超大响应和字段版本变更。一个边界函数的价值,是把不可信数据挡在业务计算之前,并让故障能被定位到明确的一层。

五 错误分类要帮助定位

HTTP 503、无法解析 JSON 和价格字段类型错误,可能来自不同故障。业务层可以把它们映射成不同的内部错误类别,同时向用户显示简短提示。不要把所有失败都改成空商品对象,因为后面的价格计算会失去“数据根本没有加载成功”这一事实。必要时记录非敏感的错误类别和字段名,而不是完整响应。

当接口允许扩展字段时,白名单返回可以降低无意传播额外信息的机会;当协议要求严格一致时,则应把未知字段也视为不兼容。两种做法都可能合理,需要由调用方契约决定。若项目已经采用成熟的运行时校验库,让 Claude 复用现有方案,避免维护两套字段规则。无论使用哪个库,都要保留字符串金额与字符串布尔值这类反例。

方法参考:Anthropic 提示工程概览。