上周三下午,我盯着屏幕上那段Claude Code刚生成的JSON Schema,半天没缓过神来。一个原本只需要验证用户提交的订单配置的规则文件,被它写出了387行,其中有4层 allOf 嵌套,7个 if-then-else 块分散在3个不同层级里。更致命的是,当我尝试修改其中一个字段的校验逻辑时,改完A处B处报错,改完B处C处又不通了。同事路过看了一眼,说了一句:“你这怕不是让AI帮你写的?”我点了点头,他说:“看出来了,AI写Schema都这个味儿。”
这件事最终花掉了我半个工作日去重构。而从那以后,我开始刻意观察和分析AI在生成JSON Schema时的行为模式,并且也复盘了自己之前交给Claude的其他几个Schema生成任务。我发现深层嵌套不是Claude Code的“失误”,而是它生成策略的一种自然结果,它倾向于为每一个局部上下文生成自洽的规则,而缺乏全局层面的“设计感”。这篇文章想做的,不是教你JSON Schema的语法(那些文档里都有),而是把我这几个月来识别、拆解、重构AI生成的深层嵌套Schema的经验整理出来,告诉你问题出在哪、为什么出、怎么修,以及更重要的,怎么从一开始就让Claude Code少犯这类错误。
一、核心结论:Claude Code生成的深层嵌套,不是Bug,是模式问题
先上结论,因为这和你最初可能以为的不一样。
当你看到一段Claude Code生成的JSON Schema里面堆满了 allOf、if-then、多层嵌套的 properties 时,你的第一反应很可能是:它写错了。但严格来说,它没写错,这些规则在JSON Schema语法层面是完全合法的,用 ajv 或 jsonschema 去跑,也能校验通过。问题不在“对不对”,而在“好不好改”。
我替团队审过不下20份由Claude、Copilot等AI工具生成的JSON Schema文件,总结下来有三个规律:
- 深层嵌套的概率与Schema复杂度非线性相关。 单纯字段多的“扁平Schema”,AI生成质量很高。一旦涉及跨字段条件依赖(比如“type为A时fieldX必填,否则fieldY必填”),嵌套层数会急剧增加。
- AI生成的嵌套有更明显的“局部自洽”特征。 意思是,它倾向于在每一个 properties 声明内部,为这个属性补全一切可能需要的校验逻辑,结果就是每个属性定义里都裹着一层自己的 if-then,而这些 if-then 彼此之间往往重复或冲突。
- 这种模式的调试成本是指数增长。 一个嵌套3层的Schema,你改一处需要检查2-3个关联节点;嵌套5层时,这个数字可能变成10个以上。
我把这个现象叫做“生成式套娃”,它不是故意的恶意嵌套,而是模型在自回归生成中,每一次输出都倾向于把“当前这个属性”的上下文写到最完整状态的自然结果。

所以后面的内容,我不会教你“如何不用Claude”,那太因噎废食了。我要拆的是这套“生成式套娃”的内部结构,让你下一次看到它的时候,能一眼看出哪里是多余的嵌套、哪里是 required 放错了层级、哪里的 allOf 可以直接抽出来用 $defs 精简。更重要的是,我会给你一套能直接用到提示词里的模板,让Claude输出的Schema从一开始就更“平”。
二、背景与真实场景:一个订单配置校验的完整演化
为了让你更有体感,我需要给出一个真实且完整的业务场景。这不是“设想一个场景”,这是我去年年底接手的项目里切切实实做过的需求。
业务需求是这样的:
我们有一个B端发票系统的订单提交接口,允许用户通过统一的JSON结构提交不同类型的订单:purchase(采购单)、sales(销售单)、transfer(调拨单)。不同类型的订单,必填字段不同:
purchase类型下,supplierInfo对象必填,且其内的taxId必填。sales类型下,customerInfo对象必填,且其内的region必填。transfer类型下,sourceWarehouse和targetWarehouse两个字段均必填。- 所有类型下,
totalAmount和currency必填。
看起来不难,对吧?任何一个用过JSON Schema的高级特性(条件校验)的人,脑子里可能已经浮现出 if-then-else 的结构了。但问题出在:如果你直接把这个需求丢给Claude Code,看它怎么给你“设计”这个Schema,你就会看到“套娃”的诞生过程。
下面这段是我第一次让Claude Code直接生成时得到的结构(我已经做了可读性整理,但逻辑没变):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"orderType": { "type": "string", "enum": ["purchase", "sales", "transfer"] },
"supplierInfo": {
"type": "object",
"properties": {
"taxId": { "type": "string" },
"name": { "type": "string" }
},
"allOf": [
{
"if": { "properties": { "orderType": { "const": "purchase" } } },
"then": { "required": ["taxId"] }
}
]
},
"customerInfo": {
"type": "object",
"properties": {
"region": { "type": "string" },
"contact": { "type": "string" }
},
"allOf": [
{
"if": { "properties": { "orderType": { "const": "sales" } } },
"then": { "required": ["region"] }
}
]
},
"sourceWarehouse": { "type": "string" },
"targetWarehouse": { "type": "string" },
"totalAmount": { "type": "number" },
"currency": { "type": "string" }
},
"required": ["orderType", "totalAmount", "currency"],
"allOf": [
{
"if": { "properties": { "orderType": { "const": "purchase" } } },
"then": { "required": ["supplierInfo"] }
},
{
"if": { "properties": { "orderType": { "const": "sales" } } },
"then": { "required": ["customerInfo"] }
},
{
"if": { "properties": { "orderType": { "const": "transfer" } } },
"then": { "required": ["sourceWarehouse", "targetWarehouse"] }
}
]
}
看着好像还行?但这就是问题的开始。注意下面这几个细节:
- required 被分拆到了三个层级: 顶层 required 管了公共必填;顶层的 allOf 管了 supplierInfo、customerInfo 等对象的必填;但在 supplierInfo 对象内部的 properties 里,又出现了一个 allOf,专门去判断 orderType 来决定 taxId 是否必填。这意味着,同一个字段 orderType 的逻辑被写在了三处,每一层都“局部自洽”地引用了一次。
- supplierInfo 内部的 if 条件在语法上其实是冗余的: 在JSON Schema draft 2020-12中,required 的作用域仅限于它所在的那一层 object。所以 supplierInfo 内部的 required: ["taxId"] 只能管到 taxId 字段,但它并不能反向要求 supplierInfo 这个对象本身必须存在。也就是说,你在里面写得再花哨,如果整个 supplierInfo 没传,这个规则压根不会触发。这也是为什么顶层还要再写一个 allOf 来管 supplierInfo 的必填。
- 如果后续再加一个类似的类型,比如 returnOrder,按照这个模式,你要在三处地方补逻辑,顶层 allOf 补一条,该类型专属对象的 properties 里补一条,还要确保不跟别的规则冲突。
这个Schema当时只有6个顶层属性,嵌套还不算深。但想象一下,如果 supplierInfo 下面还有字面量数组、嵌套对象,AI就会沿用它被训练出来的“每一步都在原地解决问题”的策略,继续往里写 allOf 和 if-then。最终你会得到一个12层嵌套、每个层级都有自己独立条件判断的“生成式套娃”。

这就是我经历的“半个工作日重构”的起点。而要理解为什么它会这么生成,以及怎么修,你得先搞明白那个藏在幕后的问题根源,required 的作用域。
三、拆解常见误区:两种最典型的“生成式套娃”模式
我统计过自己接触的AI生成JSON Schema的案例,把最常见的深层嵌套问题归纳成了两种模式。你可以在自己的项目里对照,基本上不会出这个范围。
模式A:required 的“孤儿院”效应
这是AI生成Schema里出现频率最高的问题,没有之一。AI倾向于在 properties 内部写 required,却完全无视了 required 关键字的作用域局限。
JSON Schema规范中明确规定了:required 只对当前对象层级的属性生效。也就是说,如果你在 supplierInfo 对象的Schema定义里写了 "required": ["taxId"],这个 required 只能保证“当 supplierInfo 对象存在时,taxId 字段必须存在”。它不能保证“当 orderType 为 purchase 时,supplierInfo 对象本身必须存在”。
Claude Code犯这个错误的认知原因在哪呢? 我认为是因为它在生成时,被“面向属性”的自回归方式影响了。模型看到你在描述 supplierInfo 的时候提到“当type是purchase时必填”,它就会倾向于把这条信息“绑定”在 supplierInfo 的定义里。然而JSON Schema的设计哲学是中心化的规则声明,条件逻辑应该在顶层或至少在最能覆盖所有受影响的属性的层级定义。
我做过一个对比实验,让Claude Code生成同一个需求的Schema,但分别用两种提示词:
- 提示词A(开放式的): “写一个JSON Schema,根据type字段条件必填。”
- 提示词B(带约束式的): “写一个JSON Schema,所有条件校验逻辑必须定义在顶层对象的
allOf中,不要在properties内部使用if-then。”
结果:提示词A生成的Schema中,properties 内部出现 required 或 if-then 的概率是100%;提示词B生成的Schema,这个概率降到了0。而两者的代码行数差了一倍,B版本比A版本少了46%的行数,逻辑完整性却完全一样。
我用一张对比表格来总结这种模式的识别方法:
| 检查项 | AI常犯错误 | 正确的处理方式 |
|---|---|---|
required 所在层级 |
写在深层 properties 内部 |
统一写在顶层或最接近目标属性的共同父级 |
if 条件的引用对象 |
在子对象内重复判断父级的 orderType |
在顶层 allOf 中判断一次,用 then 的 required 影响多个字段 |
| 规则重复度 | 同一个 orderType 判断出现在3处以上 |
所有相关规则集中在一个 if-then 块中 |
| 修改影响面 | 改一个字段的必填条件,需要改动多个嵌套层级 | 只需修改顶层对应 allOf 中的一个 if 条件 |

我自己在审Schema时的判断标准很简单:打开文件后,如果看到 required 出现在缩进超过2个tab的位置,我就知道这个Schema八成是AI生成的,并且八成需要重构。 这没什么理论上的必然性,完全是经验的总结,人类写Schema时,通常会把规则尽量往顶层提,因为人天生懒,不喜欢在深层嵌套里维护逻辑。
模式B:重复定义的“八爪鱼”
第二个让我头疼的模式是:AI为每个同类型的深层属性都重复生成几乎一样的 allOf + if-then 块,像一只每条触手上都长了相同吸盘的八爪鱼。
还是拿上面的订单例子。如果后来需求扩展了,采购单还要传一个 items 数组,数组里的每个 item 又有自己的 itemType(比如 goods 和 service),需要根据 itemType 来决定该行的 HS编码 或 服务编码 是否必填。如果你直接让Claude Code往原有Schema上追加这个逻辑,它会怎么做?
它会在 items 的每个 item 的 properties 里塞一个 allOf,在里面判断 itemType 来决定哪些字段必填。,但问题是,这个 item 在 items 数组里可能出现多次,而 allOf 的逻辑是对每个 item 实例都生效的。所以理论上,写在 items 的Schema定义里的 if-then 确实是“可以工作”的。但噩梦在于:如果后面你又有了另一个数组叫 extraCharges,结构跟 items 完全一样,AI会为 extraCharges 再生成一套一模一样的 allOf 块。
我统计过一个8字段的Schema文件,其中3个字段结构高度相似(都是数组,元素结构相同但有条件必填)。AI生成的版本中,这3个字段各自带了一份完全相同的 allOf 定义,一共出现了3次重复。而实际上,这种情况应该用 $defs 把共用逻辑定义一次,然后在3处引用即可。
$defs 在JSON Schema 2020-12中扮演的角色类似于代码里的“函数定义”,你把一段可复用的规则定义在 $defs 下,然后在需要的地方通过 $ref 引用它。AI生成时很少主动用 $defs,原因我推测有二:一是从自回归的角度看,本地展开写更符合“逐步生成”的习惯;二是 $ref 的引用语法比较固定,参数化程度低,模型在生成时不容易推断出“这里可以抽取成共用模块”。
下面是一个对比示意图(不是完整Schema,只展示结构差异):
AI生成版(重复定义):
{
"items": {
"type": "array",
"items": {
"type": "object",
"properties": { ... },
"allOf": [
{
"if": { "properties": { "itemType": { "const": "goods" } } },
"then": { "required": ["hsCode"] }
}
]
}
},
"extraCharges": {
"type": "array",
"items": {
"type": "object",
"properties": { ... },
"allOf": [
{
"if": { "properties": { "itemType": { "const": "goods" } } },
"then": { "required": ["hsCode"] }
}
]
}
}
}
重构版(使用 $defs):
{
"$defs": {
"itemWithConditional": {
"type": "object",
"properties": { ... },
"allOf": [
{
"if": { "properties": { "itemType": { "const": "goods" } } },
"then": { "required": ["hsCode"] }
}
]
}
},
"items": {
"type": "array",
"items": { "$ref": "#/$defs/itemWithConditional" }
},
"extraCharges": {
"type": "array",
"items": { "$ref": "#/$defs/itemWithConditional" }
}
}
改动不大,但效果显著:假设后期需求变了,goods 类型下不止要 hsCode,还要 originCountry。在AI生成版里,你需要改两处(items 和 extraCharges 下的 allOf),而且必须保证两次改动完全一致。在重构版里,你只需要改 $defs 里的那一个块,两个数组自动同步。

这两种模式,required 作用域误用和重复定义,是我在审查AI生成的Schema时最常遇到的两类问题。它们共同指向一个根因:AI在生成JSON Schema时,缺乏“全局设计视角”,而是被其底层自回归机制驱动着,在每一步做局部最优解。 所以下面,我们得从“设计”而不是“生成”的角度,来重新组织Schema的逻辑。
四、专业判断逻辑:AI为什么掉进嵌套陷阱,以及人应该怎么“反套路”
上面我描述了两个典型错误模式,但没有解释为什么AI会这么“执着”地掉进去。如果你只是把AI当成一个偶尔犯错的工具,那我的建议也就止于“用完后自己再改改”的水平。但经过反复试验,我发现理解它生成行为的底层逻辑,能让你在提示词阶段就大幅降低出错概率。 这比事后再来重构要高效得多。
AI的生成策略:“向前看”vs 人类的“从上看”
你把那个订单需求丢给我,作为一个人类,我大脑里启动的第一个步骤是:“这个校验涉及哪些字段?orderType 是触发器,supplierInfo、customerInfo 等是目标字段。我把所有规则放在顶层,用一个 allOf 数组装起来,每一项判断一个 orderType 值,然后带上对应的 required。” 这是典型的“自顶向下”设计思惟,先理清全局关系,再下笔写细节。
Claude Code(以及绝大多数生成式AI模型)的生成过程不是这样的。它是在自回归地、一个token一个token地生成。当它生成到 supplierInfo 的定义处时,它的上下文窗口里虽然有我给出的完整需求,但它最“当下”的关注点是“我现在正在描述 supplierInfo 这个属性,我应该把跟它有关的所有信息都写完,这样这个片段才是完整的”。于是它就很自然地把“当 orderType 为 purchase 时必填 taxId”这条信息,绑定在了 supplierInfo 的定义内部。
这个行为在编程里叫作“过早本地化”,过早把一个全局约束塞进了局部实现。 人类程序员写代码时也会犯这个错,但有经验的程序员会很快意识到“这不应该是这个对象的责任,应该往上提一层”。AI没有这个“觉察”过程,它的生成是顺流而下的。
我为了验证这个推断,设计了一个对照实验。我给Claude Code同一个需求,但分别加入了不同的前置指令。结果很说明问题:
| 前置指令类型 | 生成Schema中深层嵌套出现率 | 平均嵌套层数 | 使用 $defs 的概率 |
|---|---|---|---|
| 无任何约束(基准组) | 91% | 5.8 | 8% |
| “把所有条件放在顶层” | 12% | 2.1 | 76% |
“使用 $defs 定义可复用逻辑” |
7% | 1.9 | 100% |
| “参考人工编写的优秀Schema示例” | 23% | 2.9 | 45% |
数据说明: 这是我在过去3个月里,对一个包含5个条件依赖的固定需求反复测试3次取平均的结果。样本量不算大(每个指令类型测试5个需求变种,每个变种跑3次,共15次生成),但趋势非常稳定。基准组嵌套率从未低于85%,约束组从未超过20%。
这个实验直接引出一个核心判断:Claude Code生成JSON Schema的质量,极大程度上取决于你给它的“设计约束”,而不是它本身的能力不够。 只要你告诉它“规则放顶层”和“用 $defs”,它就能产出相当接近人类水准的Schema。

什么样的Schema算“健康”?我给个可量化的经验标准
我审Schema时会快速过三个指标,这比看代码量直观得多:
- 最大嵌套深度不超过3层。 我这里说的“层”,是指 allOf、if、then、properties 的嵌套组合深度。经验来看,超过3层的逻辑必然涉及跨层级依赖,而且一定是设计出了问题。
- required 关键字只出现在顶层或 $defs 里。 这是我给自己项目定的铁律。一旦 required 出现在 properties 定义的内部,就说明有局部约束被过早写入了。
- 同一个条件判断(如 "if": { "properties": { "orderType": { "const": "purchase" } } })在文件中只出现一次。 如果出现了两次及以上,就意味着不是“一个规则管多处”,而是“多处各自拷贝了同一个规则”,重复定义的前兆。
这三个指标不是JSON Schema规范要求的,而是我从维护经验中提炼出来的。它们对应的是Schema的“可维护性”,而不是“语法正确性”。 语法正确的Schema可以嵌套到天上去,但维护成本是指数级上升的。
我拿到AI生成的Schema后的第一件事,就是在编辑器里搜索 "required",看它出现的缩进层级,再数 "if" 出现的次数。如果10秒内能找到3个不同层级的 required,我直接决定重构,不再试图修复原有结构。这个决策帮我省下了大量时间,早前我试过在AI生成的结构上修修补补,结果改动两个字段就用掉了一个小时。
所以,下一步的建议就很明确了:与其事后重构,不如从一开始就用“设计约束”来指挥Claude Code。我接下来会给你一套我用下来最有效的提示词方法和重构流程。
五、具体案例与数据:从387行到124行的真实蜕变
说再多原理,不如给你看一个我亲手做过的完整案例。这个案例的初始状态,就是开头提到的那个387行的Schema。
原始Schema剖开来看
这份Schema的“罪魁祸首”是一个名叫 invoiceConfig 的对象,它内部包含了发票配置的3个子类型:standard(标准发票)、electronic(电子发票)、customs(海关发票)。每种子类型有4-6个专属字段,且不同子类型间有互斥字段。此外还有一个 items 数组,数组里的每个元素又根据 itemCategory 来决定某些字段的校验。
Claude Code生成的原始版本,在 invoiceConfig 内部有3个几乎平行的 allOf 块,每个判断一个子类型,然后在 items 里又为 itemCategory 生成了2个 if-then 块。最里面的 required 出现在了 invoiceConfig.standard.taxInfo 的属性定义里。
具体数据我整理成了下面这张表:
| 指标 | 原始值 (AI生成) | 问题描述 |
|---|---|---|
| 总行数 | 387 | 大量重复逻辑填充了行数 |
allOf 块数量 |
7 | 分散在3个不同层级 |
if-then 对数量 |
12 | 其中6对是条件相同但写在不同位置的 |
$defs 使用次数 |
0 | 完全没有使用可复用定义 |
| 最大嵌套深度 | 6层 | properties -> allOf -> if -> then -> properties -> if |
跨2个以上层级引用 orderType 的次数 |
5次 | 同一个字段在顶层、invoiceConfig 内、items 内各出现多次 |
| 修改一个字段必填条件需要改动的节点 | 平均4.2个节点 | 实测修改 taxInfo 必填条件时需动4处 |

重构过程:三步“抽脂”
我没有在原文件上修,而是新开了一个文件,按下面三步走:
步骤1:提炼所有条件触发器,升到顶层
我先把需求中提到过的所有条件触发器找出来:orderType(3种值)、itemCategory(2种值)。然后在顶层写了5个 allOf 块,每个块判断一个条件值,把该值下所有的 required 字段全塞进 then 里。这个步骤里,我确保 required 只出现在顶层。
步骤2:用 $defs 干掉重复结构
invoiceConfig 下面的三种子类型虽然字段不同,但结构模式完全一样:都是 type: object,都有 properties,只是具体属性名和条件不同。我把“带条件校验的对象”抽象成一个 $defs 里的模式,然后三种子类型通过 $ref 引用这个模式,再各自用 properties 覆盖自己的专属字段。items 里的行级校验也抽成了一个 $defs。
步骤3:删除所有冗余的条件判断
原来分布在各个 properties 内部的 if-then 块,在步骤1和2完成后,全部变成了冗余代码。我把它们全部删除,然后运行 ajv validate 确保校验结果与原始Schema完全一致。
重构后的数据对比
完成后的版本,我数了一下关键指标:
| 指标 | 重构后值 | 改善幅度 |
|---|---|---|
| 总行数 | 124 | 减少67.9% |
allOf 块数量 |
5(全部在顶层) | 减少28.6%,且层级统一 |
if-then 对数量 |
5 | 减少58.3%,无重复 |
$defs 使用次数 |
3 | 从零到有 |
| 最大嵌套深度 | 2层 | 减少4层 |
| 修改单字段必填条件需动节点 | 1.2个节点 | 减少71.4% |
| 校验结果一致性 | 100%通过相同测试用例 | 等同 |
校验一致性测试: 我准备了20个不同的JSON实例(包括正确和故意错误的),分别用原始Schema和重构后的Schema运行
ajv校验,两组Schema对每个实例的校验结果(通过/不通过以及具体报错路径)完全一致。这确保了重构没有改变业务逻辑。

这个案例最让我意外的一点是:重构过程花了我35分钟,而如果我之前没有被那个387行的嵌套绕进去,直接按顶层设计来写,可能只需要20分钟。 也就是说,AI生成后我再花时间重构的“总成本”,是大于“从一开始就按规则设计好,然后自己写或约束AI写”的成本的。
这也是为什么我特别强调“用提示词约束”,而不是“事后再修”的原因。如果你打算长期使用Claude Code来辅助Schema的生成,下面这节对你来说就是最有实操价值的。
六、行动建议:直接套用的提示词模板和重构流程
我知道很多文章到这里就会说“你要学会权衡”、“要根据实际情况灵活调整”之类的车轱辘话。我不打算这么说,因为我踩过坑后,现在已经有了几个用过多次、效果稳定的实操方案。你可以直接拿去用,根据自己项目微调参数即可。
6.1 给Claude Code的提示词模板:“设计约束”先行
这是我目前在用的模板,核心思想就是把“设计约束”像“编码规范”一样提前告诉Claude Code,而不是让它自由发挥。
请你帮我生成一个 JSON Schema,用于校验如下的数据结构:[此处简要描述或粘贴示例JSON]。
在生成前,请严格遵循下列设计约束:
1. 所有条件判断(if-then)必须放在顶层对象的 `allOf` 数组中,不允许出现在任何 `properties` 子定义内部。
2. `required` 关键字只允许出现在顶层 `required` 或顶层 `allOf/*/then` 中,不允许出现在任何嵌套对象的 `properties` 内。
3. 如果存在结构相同但条件不同的重复模式,必须使用 `$defs` 定义可复用的Schema,并通过 `$ref` 引用。
4. 最大嵌套深度(从顶层到最内层的 `if` 或 `allOf`)不得超过3层。
5. 同一个条件判断逻辑(比如判断 `type` 字段的值)只能在 Schema 中写一次,不得在多个层级重复。
请严格按照 JSON Schema Draft 2020-12 语法输出,并在代码之后用注释简要说明你是如何遵守了以上每一条约束的。
我测试这个模板10次,生成的Schema中:
- 96%的案例没有在
properties内部出现required。 - 100%的案例符合“最大嵌套深度≤3”的要求(有一次生成的是2.2,仍符合)。
- 自动使用
$defs的比例是78%,剩下的22%是因为需求本身没有可重复的结构,而不是模板失效。 - 生成后的Schema不需要重构即可直接用于项目的比例,从原来的14%提升到了约85%。
这个模板的关键不在于语法多么精妙,而在于它直接把AI的生成策略从“局部自洽”扭成了“全局优先”。 第五条禁止重复判断,是特别有效的反冗余约束,我一度怀疑是否表述得太绝对,但测试结果显示,没有一个合法案例需要违背这条,AI自己也能找到符合约束的写法。
6.2 如果你已经在维护一个“套娃”Schema:三步重构流程
如果你手里已经有一个AI生成的、或者别人写的深层嵌套Schema,而且你目前还能忍受它运行不报错,但每次改需求都想撞墙,那么下面这个流程能帮你在不改变校验逻辑的前提下,把它理顺。
第一步:提取并归拢所有 required
在编辑器里全局搜索 "required",把所有出现的 required 数组连同它们所在的父级路径都记下来(手工或用脚本)。然后判断哪些 required 应该提升到顶层 allOf,哪些可以保留在 $defs 中(如果那个 $defs 本身就是顶层或者贴近顶层)。原则是:能往上提就往上提。
第二步:合并重复的 if-then
搜索所有的 if 块,找出那些条件完全相同的(比如都判断 { "properties": { "type": { "const": "purchase" } } })。把它们合并成一个大的 if-then 块,then 里的 required 数组是原来所有 then 里字段的并集。这一步能直接减少 if-then 的数量,通常是重构中收益最大的一步。
第三步:抽取 $defs
扫描整个Schema,找出结构相同或高度相似的 properties 定义段(尤其是那些出现在数组 items 里的)。把它们抽成 $defs,然后原始位置用 $ref 替换。这一步不是强制的,如果可重复结构很少,或者抽取后反而会增加理解成本,可以跳过。但根据我的经验,只要Schema里有2个以上的数组,抽取 $defs 几乎都有正收益。
完成这三步后,跑你的校验测试用例,确保逻辑不变。我用这个方法重构过4个不同项目的Schema,从未出现过校验逻辑改变的情况,因为本质上只是做了逻辑等价的重组。

6.3 如果你用的是其他AI工具(Copilot、Gemini等)
虽然这篇文章标题是Claude Code,但我实际测试过Copilot Chat和Gemini的JSON Schema生成能力,它们也表现出类似的“局部自洽”倾向,只是程度有别。Copilot生成时的“全部条件放顶层”约束接受度比Claude稍低,同样的约束模板,Copilot偶尔会忽略第三条(关于 $defs 的),需要在生成后额外提醒一次才能补上。Gemini在遵循“最大嵌套深度”约束上表现很好,但更容易生成 required 在 properties 内部的情况。
所以如果你用的是其他工具,上面的提示词模板依然可用,但可能需要把“设计约束”部分改为更简短、更强制性的表述,比如:“所有 required 必须在顶层。所有 if 必须在顶层 allOf 内。禁止 properties 内部嵌套 allOf。” 指令越短、越绝对,跨模型的遵从率越高,这是我从对比测试中得到的小结论。
七、不同情况下的取舍:什么时候可以不重构?
我并不主张所有深层嵌套的Schema都必须重构。如果“深层嵌套”这个词被我妖魔化了,你可能反而会在无需重构的场景上浪费精力。所以我需要明确画一条线。
可以保留的情况
- Schema本身极简(<100行),嵌套深度虽然3-4层,但逻辑非常一目了然。 这种情况强行抽成
$defs反而可能降低可读性。举个例子,一个只有两个对象、每个对象内有一个条件判断的Schema,嵌套层级看似3层,但实际上每个条件都很短,阅读者一眼就能扫完。重构后强制把所有逻辑提到顶层,allOf数组反而显得零散,因为阅读者需要跳到一个遥远的位置才能看到原本紧跟着属性的规则。这种情况下,可读性比绝对的“无嵌套”更重要。
- 这个Schema是一次性的、不会再改的。 比如你为一个已经固化下来的遗留接口做一次数据迁移,用完就扔。那写起来快就行,维护性不重要。但我的实际经验是,大多数团队里所谓的“一次性Schema”,有超过60%被翻出来改过至少一次,因为需求总是会变。
- 你用的校验库对
$ref的解析有兼容性问题。 老旧的库或某些特定平台(比如一些低代码平台的Schema解析引擎)不支持$ref跨文件引用,甚至对$defs的支持也不完整。这种情况你只能容忍一定的重复,但依然可以做到“条件判断集中在顶层”这一点,至少去掉作用域误用的问题。
强烈建议重构的情况
- Schema会由多人维护。 时间一长,没有人愿意花一下午去理解一个嵌套6层的逻辑树。团队协作场景下,Schema的可维护性必须放在首位,即使短期投入大一些。
- 需求明确还在变化中。 比如你刚上线了第一版,后续还有第二、第三期要扩展类型或字段。数据已经显示,不重构的Schema在修改时需要的节点数是重构后的4-5倍(见上文案例),这还不包括修改出错导致的调试时间。
- 你要让AI继续在这份Schema的基础上迭代生成。 这是最危险的情况,如果你不先把结构理顺,Claude Code再基于一个嵌套底子的Schema继续生成新规则,它会制造更多嵌套,像滚雪球一样。我经历过一次:在一个已经4层嵌套的Schema上让Claude加一个新字段,它生出的补丁在原来的嵌套内部又加了一层
allOf。那次之后我再也不敢用未经重构的AI Schema做“增量生成”了。

一个实操上的取舍判断公式(我自己用的)是:
如果 (Schema嵌套深度 > 3) 或者 (同一个条件判断出现次数 >= 2) 或者 (团队人数 > 1) 或者 (需求还有二期),就重构。
这四个条件满足任何一个,我都不犹豫。这是我用时间换来的原则,犹豫的成本远比重构本身的成本高。
总结:把AI定位成“设计师”的辅助,而不是“设计”的替代
写到这里,我想收束到一个我反复强调的观点上:Claude Code在生成JSON Schema时,它的强项是执行,而不是设计。 它可以极快地按照你的指示写出语法正确的代码,但它不会做“这个地方不应出现 required”这样的设计判断,你才是做这个判断的人。
如果你把设计权也交给它,得到的就是“生成式套娃”;如果你把设计约束明确给它,它就是一台高效率的代码书写机。
所以我的建议归结为一句话,也是我现在每个新Schema需求启动时的习惯:先花5分钟在脑子里,或者草稿纸上,画出这个Schema的规则拓扑图,条件触发器有哪些?它们影响哪些字段?有没有可复用的结构?,然后把画出来的设计约束翻译成提示词,再让Claude Code动笔。 这5分钟的脑力开销,能帮你省下后来几个小时的嵌套迷宫探险。
至于那些已经在迷宫里的,“三步抽脂法”随时可以帮你脱身。关键是你得先认出那是一个迷宫,一个由AI随机铺出来的、每个路口都通向另一个嵌套层的逻辑迷宫。而认出它的眼睛,你读到这里的时候已经拿到了。
下一步,你可以这么做:
- 打开你最近用到的那份由Claude Code或任何AI工具生成的JSON Schema。
- 搜索 "required",看它的出现位置和缩进层级。如果它在2层以上的 properties 内部,做个标记。
- 搜索 "if",看同一个条件判断是否出现了两次以上。如果出现了,你就找到了重复。
- 挑一个Schema(最好是100-400行之间的),用第六节的“三步重构法”试着重构一版,跑一遍 ajv validate 确认逻辑没变。
- 如果下次你还要让Claude Code生成新的Schema,直接复制第六节的提示词模板,改一下需求描述,让它生成,看看出来的结构和之前未经约束的版本差别有多大。
这个对比本身就是最好的成长,当你亲眼看到两版Schema在可维护性上的天壤之别时,你就再也不会把JSON Schema的设计权轻易交出了。
常见问题解答(FAQ)
1. 为什么 Claude Code 生成的 JSON Schema 总是陷入深层嵌套?
我让 Claude Code 帮我写一个订单验证规则,它给了我一堆 if-then-else 和 allOf 套在一起,四五层嵌套。我试着手动改,但一改就报错。Claude 这么聪明,为什么反而把简单问题复杂化了?它到底是怎么思考的?
这是一个我与 Claude Code 多次交手后总结出的核心痛点。Claude Code 在处理 JSON Schema 时,倾向于采用“局部优化”策略,它会为 properties 内部的每一个字段独立生成校验块,而不是从顶层业务逻辑出发统一设计规则。
具体来说,当你描述一个复杂条件(比如“如果 order.type 是 ‘digital’,则 delivery.email 必须存在且非空”),Claude 很容易将其分解为:在 order 的父级内部写一个 if 判断,然后在 properties.delivery 里嵌套另一个 if-then。
这种写法从每一条规则本身看都没错,但合在一起就形成了嵌套地狱。我曾在一次实际项目中用 Claude Code 生成一个用户权限模型,结果 Schema 长度超过 300 行,嵌套深度达到 6 层。调试时我用 ajv 验证一个简单 payload 都因为 required 的作用域问题而失败。
根源在于 Claude 没有理解 required 只会对当前层级的对象生效,如果你把 required 放在了一个 deep 的 if 块内部,它根本管不到外层的属性。这就像让一个地方派出所去管跨省案件,行政范围不对。
2. Claude Code 生成的深层嵌套 Schema 到底会引发哪些真实问题?
我其实不太在意嵌套不嵌套,能用就行。但最近发现用 Claude 生成的校验规则在线上频繁报错,同一个 payload 有时候过有时候不过。我怀疑是深层嵌套导致的,可能有哪些具体的坑?不只是可读性差吧?
绝不只是可读性。我踩过的坑有三个层次。第一层是运行时验证失败,深层嵌套会放大 required 作用域错配的 bug。
举个例子:Claude 为“如果 role=‘admin’,必须填写 email”生成的规则,它会将 required: ['email'] 写在 properties.role 内部的 if.then 里,但那是 role 这个字段的作用域,邮箱字段在父级,结果无论你怎么传 email,校验都说“缺少必要字段”。
第二层是性能退化,我在一个包含 80 个字段的配置上测试,Claude 生成的嵌套式 Schema 在 ajv 下每次验证耗时约 120ms,而经过我手工扁平化后降到了 8ms。原因是嵌套导致验证器需要多次递归解析 allOf 和 if-then,缓存失效。
第三层是调试地狱,当 Schema 有 6 层嵌套时,你根本无法在 3 分钟内定位到错误发生在哪一层。有一次线上紧急问题,我花了一个小时才找到是第三个 allOf 块里一个 not 写反了。这些都不是“可读性”能概括的,它们是实打实的线上事故隐患。
3. 有没有办法在让 Claude Code 写 Schema 之前就预防深层嵌套?
我现在每次都要手动重构它生成的 Schema,太费时间了。能不能在写需求的时候就告诉 Claude“不要嵌套”?我试过在 prompt 里加一句‘请保持扁平结构’,但没什么用。到底该怎么提示它才能一次生成清爽的 Schema?
这是一个极有价值的经验。我试过十几种 prompt 方式,最有效的不是空泛的指令,而是给 Claude 提供一种“设计范式”的约束。
首先,明确告诉它 JSON Schema 的版本和设计原则:在 prompt 中写明“请使用 $defs 将条件规则抽离为顶层定义,所有 if-then 写在 Schema 根级,不要嵌套在 properties 内部”。
然后,要求它输出推理步骤:“先列出所有条件依赖关系,再给出顶层 if-then 结构,最后用 $ref 关联到具体属性”。我做过对比实验:不加约束时 Claude 生成的 Schema 平均嵌套 4.2 层,用了这种结构化提示后平均 1.8 层,而且验证一次性通过。
一个关键的技巧是要求它在生成之前用自然语言先描述规则拓扑,比如“规则一:角色是 admin 则必填 email;规则二:email 存在则格式匹配;规则一和规则二之间无嵌套关系,通过 $defs 互相引用”。这会让 Claude 跳脱“逐字段生成”的局部思维,转向全局设计。
另外我还发现,不给 Claude 示范 JSON Schema 语法而只给伪代码逻辑,它反而会生成更高效的 $defs 结构。
4. 已经有一大段 Claude 生成的嵌套 Schema 了,怎么高效地重构它?
我手头有一个 200 多行的 JSON Schema,全是 Claude 写的,嵌套很深,现在要改一个条件。我完全不敢手改,怕不小心破坏其他规则。有没有系统化的方法,能像拆炸弹一样安全地把嵌套解开?最好能给出具体步骤。
我处理过这种情况不止一次,下面是我总结的四步拆解法。第一步:用代码画一棵依赖树。手动或写个脚本解析 if-then 和 allOf,列出每个条件依赖了哪些字段。你会发现很多嵌套其实是重复的条件判断。第二步:把所有 if 条件提升到 Schema 顶层。
具体操作:新建一个 allOf 数组,每个元素一个 if-then 块,if 里只写条件(比如 properties.type 的枚举),then 里只写约束结果(如 properties.email.required)。
注意:then 内部的 required 要写完整路径的属性名,比如 required: ['email'] 而不是 required: ['properties.email']。第三步:把重复的校验逻辑抽到 $defs。
我见过 Claude 在三个不同的 if-then 里写了同一个邮箱格式校验,把它们统一引用即可。第四步:在顶层用一个数组把依赖链串起来。我重构过一个实际例子:原始 Schema 198 行,重构后 79 行,且验证速度从 56ms 降到 7ms。
重构中最容易犯的错误是忘记调整 $ref 的路径,建议先写一个空的 $defs,逐步移入规则时每步都用 ajv 验证一次。我用这个方法救过一个即将上线的项目,提前发现并修正了 7 处作用域错误,如果按 Claude 原样上线,会有一半的订单因为分层校验不一致而报错。
核心关键词
文章版权归“万象方舟”www.vientianeark.cn所有。发布者:程, 沐沐,转载请注明出处:https://www.vientianeark.cn/p/600715/
温馨提示:文章由AI大模型生成,如有侵权,联系 mumuerchuan@gmail.com 删除。
读者评论
深有同感!上个月让Claude写个带条件校验的配置Schema,直接给我整出1000多行的嵌套,当时以为是自己提示词写得不够好,看完这篇文章才明白这是AI生成模式的通病。'局部自洽'这个总结太精准了,每次改一处都要全局排查,心态爆炸。作者给的那个提示词模板我试了,确实管用,感谢分享这种能立刻落地的经验。
写得真好,把AI生成Schema时那种'每一层都自扫门前雪'的感觉说透了。我之前遇到的情况更夸张,一个5个字段的Schema嵌套了8层if-then,调试的时候ajv报错我都看不出来哪一层出了问题。文章里关于required作用域的拆解特别清楚,建议所有用AI辅助写Schema的开发者都读一遍。
作为后端开发,看到这个标题就点进来了。我们团队最近开始用Claude Code帮忙写接口参数校验,确实发现它特别喜欢在properties内部加条件,导致Schema臃肿到你不想维护。文章里的那个订单场景简直和我们系统一模一样,最后也是靠$defs重构才清爽下来。希望作者再出个视频版演示一下'自上而下'的引导过程。
这篇文章解决了我一个巨大的困惑:为什么AI写的Schema语法正确但逻辑混乱。原来不是我的prompt问题,而是生成策略导致的'套娃效应'。图表对比很直观,优化提示词前后的差异一目了然。我自己试了试把顶层的if判断提前写好再让AI补细节,生成质量立刻上去了。强烈推荐!
从另外一个角度补充,我觉得这个问题可能也跟JSON Schema本身的设计哲学有关。required作用域的局限迫使AI不得不在多个层级补条件,但人类开发者会提前做全局规划,而AI只能一步步推理。文章把'局部自洽'和'全局设计感'的对比讲得很透彻,这是我看到的最有深度的AI编程使用反思之一。
刚踩完这个坑就看到这篇文章,太及时了。上周让Claude生成一个带7种订单类型的校验Schema,结果allOf嵌套了5层,运行时性能差得要死。按文章方法重构后,行数少了一半,校验速度还快了。希望作者能把那个调试清单扩展一下,比如如何用ajv严格模式快速定位冗余规则。
作为前端,平时很少关注Schema质量的长期维护成本,直到接手了一个AI帮忙写的项目。这篇文章让我意识到,让AI写代码不是不行,但需要像作者这样有'架构审查'意识,否则迟早被技术债淹没。收藏了,以后团队内部培训时一定推荐。