JSON Schema 不是同一件东西——Anthropic、OpenAI、Gemini 强制的是不同子集,一移植 schema,就会撞上这个不匹配。
Anthropic 在校验时会忽略 minLength、maxLength、minimum、maximum。Gemini 只强制 OpenAPI 3.0 子集。OpenAI 的 strict 模式要求 additionalProperties: false,并把所有字段标为 required。每家都有文档,都不是 JSON Schema draft-2020-12,且当你发一个它不强制的关键字时,都没有一家会大声报错——schema 被接收、约束悄悄消失、模型愉快地产出 schema 本应禁止的输出。这篇是你跨厂商移植 schema 时放在编辑器旁边的那张表——各家强制什么、会静默丢什么,以及三家都能扛住的可移植子集。
三个子集的鸟瞰。
Anthropic、OpenAI、Gemini 都把 JSON Schema 塞进同一个语法槽——分别叫 input_schema、parameters、parameters——三家都称之为 "JSON Schema"。含义的差别足以让"为一家写的 schema"到另外两家都成为候选 bug。K7 厂商矩阵覆盖外层包装差异;本文往里再一层,聚焦 schema 关键字的强制。
三种形状先钉。Anthropic 把 "JSON Schema" 当作一份很宽松子集的粗略描述——schema 记录意图,模型的受限解码器兑现某些关键字、把另一些视作文档。OpenAI 有两种模式:非 strict(schema 是模型尽量遵循的提示)与 strict(schema 由受限解码器强制)。Gemini 用 OpenAPI 3.0 的 Schema 对象——一个定义良好的 JSON Schema draft-04 子集,缺少 draft-2020-12 新增的很多东西,一个可见的表面差异是类型名大写。
模式与契约一文覆盖设计原则——让非法状态无法表示、让安全相关字段必填——这正是 K10 纪律最要紧的地方:一个在目标厂商上静默丢失的关键字,就是一条模型不再兑现的承重约束。结构化工具 I/O一文覆盖同一故事的返回侧。这里我们把镜头拉近到你移植时需要的关键字表。
Anthropic 子集:字符串与数字在边界开放。
Anthropic 的 input_schema 兑现结构性关键字——type、properties、required、enum、items、description——并丢弃范围类关键字:字符串的 minLength、maxLength,数字的 minimum、maximum、exclusiveMinimum、exclusiveMaximum。它也不在受限解码器层强制 pattern 或 format。schema 文本里模型会读到它们、通常会遵守,但没有硬 mask 反违反——约束只是劝告。同理,const 不被强制;若约束是承重件,请使用只有一个元素的 enum。
实操结果是:一份在 OpenAI strict 模式下明显把字符串封顶到 200 字符的 schema,在 Anthropic 上会静默接收 5000 字符。若上限重要——因为下游要存进一个有界列,或者你按输出令牌付费——schema 就不是 Anthropic 上的强制点;你要在应用代码里校验模型输出,或者把约束表达成允许值的 enum。数字范围同理:带 minimum: 1 与 maximum: 100 的 quantity 字段在 Anthropic 上是文档而非强制,负数量将是你在数据库而非模型那里抓到的 bug。
Anthropic 会好好强制的:enum 成员资格、required 字段、顶层 type、对象形状(属性名,设 additionalProperties: false 时不允许多余)。围绕这些设计你的 Anthropic schema;其他一切当作 description 里的散文,并在外壳边缘做校验。
OpenAI 的 strict 模式:处处 additionalProperties、全部 required。
OpenAI 的 strict 模式按设计是三家里最紧的——schema 被编译成真正的语法,模型输出保证匹配。为这份保证付出的代价是两条常让团队意外的规则。第一,schema 里的每个对象都必须设 additionalProperties: false,包括嵌套的——省略它,strict 校验器就拒绝该 schema。第二,properties 里列的每个属性也必须出现在 required 里;strict 模式不像 JSON Schema 别处那样区分"可选字段"。真正可选字段的绕开法是并集类型:"type": ["string", "null"],由调用方决定 null 表示"未提供"。
由此派生出两条规则。strict 模式在大多数位置上不支持 oneOf、allOf、anyOf(截至 2026 年文档;请核对兼容性矩阵,它在演化)。深嵌套 schema 会撞到实现层的深度上限,通常在几层左右;修法通常是展平。字符串关键字都可用——minLength、maxLength、pattern、enum、format——数字关键字也可用。开启强制的开关是 strict: true;不开,schema 只是提示,模型可能产出违反它的输出。
你会看到的失效模式很隐微:你的 schema 起草时对准 Anthropic(宽松、无 additionalProperties 纪律、有些可选),打开 OpenAI strict 模式,SDK 拒绝注册工具。这是好失败——跑之前就停。坏失败是你按 OpenAI strict 起草、发给 Anthropic,字符串长度上限静默消失,因为 Anthropic 忽略这些关键字。两向都咬人;K7 一文的"中间表示"模式是机械修法。
Gemini 的 OpenAPI 3.0 子集:大写类型与"没 draft-2020-12 花招"。
Gemini 的 schema 取自 OpenAPI 3.0 的 Schema 对象,本质上是 JSON Schema draft-04 加 OpenAPI 特定关键字。类型名是大写字符串(OBJECT、STRING、INTEGER、NUMBER、BOOLEAN、ARRAY),不是小写,这是复制粘贴移植时第一件绊倒你的事。此外,OpenAPI 3.0 里不存在的构造会被拒绝:const、$defs、跨文件 $ref,以及更奇异的 draft-2020-12 新增内容都过不了校验。
Gemini 可靠强制的:type、properties、required、enum、items、description、字符串的 minLength / maxLength、数字的 minimum / maximum、常见 format。不可靠的:const、异类型的 oneOf、patternProperties。nullable: true 是一个 OpenAPI 风格旋钮,而非 JSON Schema 的 "type": ["string", "null"] 并集;把它搞错是常见移植 bug。
| keyword | Anthropic | OpenAI strict | Gemini (OpenAPI) | |------------------------|------------|---------------|------------------| | type / properties | enforced | enforced | enforced (UPPER) | | required | enforced | enforced (all)| enforced | | enum | enforced | enforced | enforced | | additionalProperties | honoured | REQUIRED false| honoured | | minLength / maxLength | ignored | enforced | enforced | | minimum / maximum | ignored | enforced | enforced | | pattern | advisory | enforced | partial | | format | advisory | enforced | well-known only | | const | ignored | enforced | rejected | | oneOf / anyOf / allOf | partial | limited | not supported | | $defs / $ref | supported | limited | not supported | | type: ["x", "null"] | supported | required opt | use nullable |
读某关键字对应的一行,就能知道它的可移植性。三列行为都不同的关键字,就是你要么在可移植规格里避开、要么放到"逐厂商 emitter"后面的关键字。写着"ignored"的部分是"静默丢弃"地带:既不报错、也不强制,你的测试套件必须在应用边界而非 schema 处抓到约束。
可移植子集:真的能不改就搬的部分。
把三列强制取交集,结果就是可移植子集。顶层 "type": "object"。一份含原语——string、integer、number、boolean——的 properties 映射,加上带 items 的原语数组。一个 required 数组。字符串上的 enum。到处 description。就这些。跨三家完好移植、三家都强制,涵盖真实工具 schema 的大部分而无需仪式。
# The portable subset — enforces the same way on all three vendors. { "type": "object", "properties": { "order_id": { "type": "string", "description": "The order identifier, e.g. 'o_42'." }, "reason": { "type": "string", "enum": ["defective", "wrong_item", "other"], "description": "The reason for the refund." }, "amount_cents": { "type": "integer", "description": "Refund amount in cents (must be positive)." } }, "required": ["order_id", "reason", "amount_cents"] }
该 schema 里缺席的内容是刻意的。amount_cents 上没有 minimum: 1,因为 Anthropic 不会强制它,而一份可移植 schema 不能在某家静默丢约束。描述里说了"必须为正"——这是模型通常会遵守的劝告性散文,应用层校验兜底。顶层没有 additionalProperties: false,因为它会在某些模式下破坏 Anthropic 的宽容校验器,并强制 OpenAI strict 模式才合法。把两者都在"逐厂商 emitter"这一步加回,不要放进共享规格。
把五步合起来读,JSON Schema 就不再是同一件东西。三家厂商、三个子集、一处可以依赖的窄交集,扩展则挂在 emitter 后面。让 schema 可移植性长期健康的团队做两件事——他们从不直接为某一家厂商起草 schema,且每次提交都跑一次逐厂商 CI 校验——两条实践搭建都要花几小时,却能在代码库一辈子里省下几周"为什么 enum 不再工作"的调试。