Gemini schema 被静默剥离怎么排查
Gemini API 的结构化输出会静默剥离 8 个 schema 关键字:minLength、maxLength、minProperties、maxProperties、uniqueItems、contains、minContains、maxContains;minItems 与 maxItems 才会被执行。(核验 2026-11-05,F14、F15)
一、Gemini 结构化输出:问题在 schema 约束会悄悄消失
你写了这样一个字段:
assignee: str = Field(min_length=6, description="受理人工号")
请求发出去了,返回来了,json.loads 通过,入库。三天后对账发现,assignee 有一批是三字姓名——你明明在 schema 里写了最少 6 个字符。
这里没有 bug,也没有报错。官方在结构化输出页写得很清楚:minLength、maxLength、minProperties、maxProperties、uniqueItems、contains、minContains、maxContains 这八个关键字不参与受限解码,请求归一化时会被自动剥掉并忽略,而且是剥离,不是返回错误(F14)。官方还专门点了一句:Pydantic 的 Field(min_length=...) 与 Zod 的 .min() 会自动生成这类关键字。
也就是说,你越是用心写约束,越可能踩到这条静默通道。好消息是同一页也说明了另一面:minItems 与 maxItems 是真正执行的(F15)。一组剥、一组留,边界很硬,但文档里只有两段话,很容易读漏。
二、Gemini 结构化输出:约束留存的最小可运行示例
排查的第一步不是改 schema,而是把它摊开看清楚:哪些约束会留下,哪些会消失。下面这段按 F14 的描述模拟了服务端的归一化行为——是模拟,不是复现。它不能证明服务端真的放行了某条数据,只能证明你的应用侧漏了校验。
## EXPECT: 被剥离 2 个:minLength、uniqueItems | 仍生效 1 个:minItems | 服务端拦截「张三丰」=False | 应用侧拦截「张三丰」=True
SILENT = {"minLength", "maxLength", "minProperties", "maxProperties",
"uniqueItems", "contains", "minContains", "maxContains"}
ENFORCED = {"minItems", "maxItems"}
SCHEMA = {
"type": "object",
"properties": {
"assignee": {"type": "string", "minLength": 6},
"tags": {"type": "array", "uniqueItems": True, "minItems": 1,
"items": {"type": "string"}},
},
"required": ["assignee", "tags"],
}
def collect(node, keys):
if isinstance(node, dict):
for key, value in node.items():
if key in SILENT | ENFORCED:
keys.add(key)
collect(value, keys)
elif isinstance(node, list):
for item in node:
collect(item, keys)
def strip(node):
if isinstance(node, dict):
return {k: strip(v) for k, v in node.items() if k not in SILENT}
if isinstance(node, list):
return [strip(x) for x in node]
return node
before, after = set(), set()
collect(SCHEMA, before)
collect(strip(SCHEMA), after)
gone, kept = sorted(before & SILENT), sorted(after & ENFORCED)
payload = {"assignee": "张三丰", "tags": ["退款"]}
server_blocks = False # 按 F14 推演:剥离后无长度约束,服务端不会拦;推演值,非实测
client_blocks = len(payload["assignee"]) < 6 # 只有应用侧还记得那条规则
assert gone == ["minLength", "uniqueItems"] and kept == ["minItems"]
print(f"被剥离 {len(gone)} 个:{'、'.join(gone)} | 仍生效 {len(kept)} 个:{'、'.join(kept)} | "
f"服务端拦截「{payload['assignee']}」={server_blocks} | 应用侧拦截「{payload['assignee']}」={client_blocks}")
输出里最关键的不是「剥了两个」,而是最后两段:同一条三字姓名,服务端不拦、应用侧拦住。schema 管形状,取值规则得你自己守。
这里必须说清一件事:server_blocks 是按 F14 写死的推演值,不是实测结果。这段代码真正证明的是「你的校验漏了」,不是「服务端真的放行了」——要证明后者,得拿一个三字姓名真发一次请求,看它是否原样回来。写代码时把这个区别标在注释里,比事后跟同事解释便宜得多。
三、解释与边界:两组关键字,两条失败路径
第一组,被剥离的八个。 minLength/maxLength 管字符串长度,minProperties/maxProperties 管对象属性数,uniqueItems/contains/minContains/maxContains 管数组元素(F14)。这些在请求归一化阶段被移除,不报错、不警告,你在响应里也看不到痕迹。能用的是应用侧的 model_validate_json()、Zod 的 .parse(),或者像上面那样手写校验。
第二组,真正执行的两个。 minItems 与 maxItems 会进受限解码(F15)。这带来一个反直觉的后果:数组「至少一项」能管住,「项与项不重复」管不住;「最多几项」能管住,「每项长度」管不住。写 schema 时按这个分界线落笔,比按 JSON Schema 的完整能力去写要可靠。
两组关键字通常不是你手写的。 用 Pydantic 的时候,Field(min_length=6) 会在生成的 schema 里写出 minLength: 6;用 Zod 的时候,.min(6) 写出的是同一个键。你看到的本意是「这个字段至少六个字符」,生成物里却是一个注定被剥掉的关键字——代码没有错,错在把校验意图寄托在了一个不执行的层上。所以排查顺序是:先看生成出来的 JSON Schema 原文,再看哪些键落在被剥的那八个里,最后把这些规则搬回应用侧。
三条自查,比读文档快。 拿到一份 schema,做三件事就能定位大部分问题:把这份 schema 打印出来,数一下八个关键字的出现次数;看每个约束是加在数组本身还是 items 里;看有没有把本该 required 的字段写成可选——可选字段会增加输出的分支,既影响约束强度也影响复杂度。
第三条路径,直接拒绝。 如果 schema 太复杂——嵌套太深、分支太多——服务端不是剥离而是拒绝,返回 400 INVALID_ARGUMENT(F16)。官方给了四条简化口径:把非结构规则挪进 description、压平嵌套并把递归引用设为可选、减少可选字段与 enum/anyOf 造成的分支、缩短属性名并去掉数组与数值边界。这四条的共同点是把「约束」从语法层挪到语义层,代价是正确性要靠应用侧兜。
边界。 本节不讨论「怎么写一个完美的 schema」——那取决于你的业务字段,没有通用答案。也不讲 400 之外的服务端错误,那些属于第 11 章。这里只给一个判断:看到某条约束没生效时,先查它在哪一组,别先怀疑模型。
四、预期输出与异常分支
真跑输出:
被剥离 2 个:minLength、uniqueItems | 仍生效 1 个:minItems | 服务端拦截「张三丰」=False | 应用侧拦截「张三丰」=True
五类分支。长度约束静默失效:min_length/max_length 是最常被写、也最常被剥的一对,短值长值都能过;这类字段的校验必须写在应用侧。去重失效:uniqueItems 被剥之后,tags 里出现两个「退款」完全正常,需要自己在入库前 set() 一次。误以为 minItems 能兜质量:它只保证「至少几项」,不保证每项合格,元素级规则要写在 items 里,而 items 里的长度约束照样会被剥。400 出现在「只是多嵌套一层」时:官方对状态机上限定得很死,深度和分支都算,遇到 400 先砍深度而不是砍字段。Pydantic 用户特有的坑:Field(min_length=6) 写起来像约束,生成的 schema 里确实是 minLength,然后在服务端被剥——代码看着没错,行为已经错了。
五、判据卡:schema 通过时还要校验什么
| 该这么做 | 不该这么做 |
|---|---|
| 请求前先数一遍哪些关键字会被剥 | 认为 schema 写进去就一定生效 |
| 长度、去重、属性数放应用侧校验 | 指望 minLength/uniqueItems 拦住 |
数组至少/最多几项交给 minItems |
拿 minItems 当元素质量检查 |
| 400 先压平嵌套与分支 | 见 400 就删字段 |
六、来源与核验
https://aistudio.google.com/docs/structured-output.md(核验 2026-11-05,F14、F15、F16、F17)
两组关键字的划分与 400 的触发条件属于「API 结构」档事实,复核周期三个月,见 1.3。若官方后续把某个关键字从「剥离」改成「执行」,本节的分组表要重写,不能只改一句话。
七、动手试
- 把
SCHEMA里tags的minItems改成2,重跑。预期仍生效仍是 1 个,但你的输出里看不出数值变化;验收标准是你能说清「为什么这个示例证明不了minItems生效」——提示:它要真的发一次请求。 - 给
SCHEMA的assignee加"maxLength": 8,重跑。预期被剥离变成 3 个;验收标准是你能列出你项目里所有会被剥的字段,并各给一条应用侧校验。 - 打开你项目的 schema 生成代码,搜
min_length、max_length、unique、min_items。验收标准:逐个标注「剥离」或「执行」;标不出来的,就是还没核过这一节。

TRAE-AI编程


更多建议: