教程
Gemini API 如何输出 JSON?Structured Output 与 JSON Schema 完整教程
不要再让模型「尽量返回 JSON」。用 Structured Output 把形状钉死,再用 JSON Schema 约束字段,下游代码才能稳定解析。
你让 Gemini 返回 JSON,它却夹一段解释、漏一个逗号,或把字段名改成「更通顺」的英文。提示词里写「只输出 JSON」只能降低概率,不能当契约。Structured Output 把契约放到解码阶段:先声明 MIME 类型,再挂上 Schema,模型按形状吐 token。读完你可以判断该用 JSON 模式还是 Schema 模式,写得出能跑的请求,并知道返回值仍要本地校验。
为什么需要结构化输出
下游一旦JSON.parse,失败就不是文案问题,而是整条流水线中断。分类接口需要固定枚举,抽取接口需要稳定键名,工具调用需要参数对象。这些场景里,模型「自由发挥」的成本很高:一次坏 JSON 就要重试、打日志、甚至让用户再点一次。
更隐蔽的失败是「能 parse,但形状不对」。例如你期望items是数组,模型给了对象;你期望score是数字,模型给了"0.9"。代码读到undefined,问题会拖到很后面才爆。Structured Output 解决的是形状,不是对错:它保证你拿到合法且符合 Schema 的 JSON,不保证分类一定正确。所以生产里仍然要校验业务规则,只是解析层会安静很多。
官方能力说明见Gemini Structured output 文档。Google 也宣布了对标准 JSON Schema 关键词与字段顺序的增强,见Structured Outputs 更新说明。写 Schema 前,建议对照Understanding JSON Schema,避免把「文档里能写的关键词」和「当前模型实际约束的关键词」混为一谈。
JSON 模式与 Schema 模式
可以把它想成两档开关。第一档只保证「这是一段能 parse 的 JSON」。第二档保证「这是符合你声明的那份 Schema 的 JSON」。代码要读具名字段时,用第二档。只有探索性抽取、键名本身也交给模型时,才考虑第一档。
| 档位 | 你配置什么 | 实际保证 |
|---|---|---|
| JSON 模式 | 仅responseMimeType: application/json |
大概率是合法 JSON,字段名与嵌套仍由模型决定 |
| Schema 模式 | MIME 类型 +responseSchema或responseJsonSchema |
形状、类型、必填与枚举按 Schema 约束 |
只开 JSON 模式时,官方也提醒这更像强提示,仍有畸形输出的小概率。要接近「总能 parse 成对象」,应同时给出 Schema。Schema 本身会计入输入 token,所以不要把说明文字再抄一遍进 prompt:重复描述会拉低质量,也浪费配额。
responseSchema 还是 responseJsonSchema
responseSchema走的是 OpenAPI 3.0 风格的 Schema 子集,REST 里类型名常见大写,如OBJECT、STRING。它适合扁平对象、枚举分类、以及需要propertyOrdering钉死键顺序的场景。短处是不吃$ref / $defs,递归树、复用定义只能靠内联,很快会膨胀。
responseJsonSchema面向 Gemini 2.5 及更新模型,吃的是更接近标准的 JSON Schema,覆盖anyOf、$ref、minimum / maximum、additionalProperties、type: null、prefixItems等。你用 Pydantic 或 Zod 生成 Schema 再塞进去,摩擦会小很多。键顺序在较新模型上会按 Schema 声明顺序保留,对日志 diff 和黄金集对比都友好。
选型可以记三条。只要扁平分类或抽取,两种都能用。需要递归、共享定义或联合类型,优先responseJsonSchema。必须钉死字段出现顺序,确认当前模型是否仍支持propertyOrdering,不要假设所有端点行为一致。
Schema 怎么写
先写你真正要读的对象,而不是「尽可能完整的世界模型」。每多一个可选字段,模型就多一次填错或填空的机会。必填项写进required,枚举写进enum,数字范围用minimum / maximum。给属性加description往往比在 prompt 里再解释一遍更稳,因为约束和解码绑在一起。
下面这份 Schema 模拟工单分类:类别只能是三个值之一,优先级是整数,摘要是字符串。这就是 Schema 模式该管的事——形状,而不是「这张工单在业务上该不该标紧急」。
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "bug", "feature"],
"description": "Ticket category"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5
},
"summary": {
"type": "string"
}
},
"required": ["category", "priority", "summary"],
"additionalProperties": false
}
数组用items描述元素。元组感更强的固定长度列表,在 JSON Schema 路径下看prefixItems。可空字段不要只靠「不写 required」,而要明确允许null,否则模型可能省略键,你的代码还在obj.field上假设键一定存在。
嵌套对象把子 Schema 内联进去即可。只有出现第三次相同结构,或树节点引用自身时,才值得上$defs + $ref。过早抽象会让报错更难读:服务端拒绝 Schema 时,你要对着一份展开后的大对象排查。
Python 与 JavaScript 实操
下面用 Google 官方 SDK 的常见写法。模型名请按你账号里实际可用的 2.5 / 更新型号替换,不要把示例里的名字当成长期钉死的生产配置。
Python:MIME 类型 + JSON Schema
from google import genai
client = genai.Client()
schema = {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["billing", "bug", "feature"]},
"priority": {"type": "integer"},
"summary": {"type": "string"},
},
"required": ["category", "priority", "summary"],
}
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Classify this ticket: invoice PDF cannot be downloaded.",
config={
"response_mime_type": "application/json",
"response_json_schema": schema,
},
)
print(response.text)
若团队已经用 Pydantic 建模,直接Model.model_json_schema()交给response_json_schema,再用model_validate_json(response.text)做本地二次校验。第一层是 API 约束形状,第二层是你的类型系统拒绝「看起来合法、语义荒唐」的值,例如优先级写成 99。
JavaScript:generationConfig
const response = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: "Classify this ticket: invoice PDF cannot be downloaded.",
config: {
responseMimeType: "application/json",
responseJsonSchema: {
type: "object",
properties: {
category: { type: "string", enum: ["billing", "bug", "feature"] },
priority: { type: "integer" },
summary: { type: "string" },
},
required: ["category", "priority", "summary"],
},
},
});
const data = JSON.parse(response.text);
REST 调用则把同样的字段放进generationConfig。OpenAPI 风格的responseSchema在部分端点仍用大写类型名,拷文档示例时不要和 JSON Schema 的小写object混用。接到响应后立刻JSON.parse,不要先用正则抠代码块:既然你声明了 JSON MIME,就按「整段都是 JSON」处理,少一层脆弱解析。
常见陷阱
- Prompt 和 Schema 各写一遍结构,模型会在两种描述之间摇摆,输出质量下降。
- Schema 过大:复杂嵌套、过深
$ref、过宽anyOf可能被拒绝,或约束变弱。先拆成小对象跑通,再合并。 - 把「真实性」交给 Schema。枚举能限制类别集合,不能阻止模型选错类别。关键路径要抽检或上规则。
- 本地类型与 Schema 漂移。Pydantic / Zod 改了字段,请求里的 Schema 还是旧的,线上会静默出现新键或缺键。
- 只测快乐路径。补两三个边界:空数组、可空字段、超长字符串、非法枚举(应被挡住)。
还有一个工程问题:日志里不要只存response.text。把所用模型、Schema 哈希、prompt 版本一起记下。结构化输出出问题,多半是「换了模型默认值」或「Schema 微改导致拒绝」,没有这三件套你会觉得「昨天还能用」。
落地:校验、对照与返工
API 返回后,先当它是不可信字节。解析、按同一份 Schema 再校验、再映射到内部类型。失败时记录原始文本(注意脱敏),决定重试还是降级。重试不要换一套完全不同的 Schema,否则你无法判断是模型不稳还是契约在变。
调试阶段把样例 JSON 贴进本站工具最省事。先用JSON 格式化看清嵌套,再用JSON 校验抓语法,最后把契约丢进JSON Schema看实例是否过关。字段突然变多或变少时,用JSON Diff对比两次响应,比肉眼扫日志快。
如果你在设计抽取管道,也可以先手写一份「理想输出」,用 Schema 工具反推类型,再把这份 Schema 贴回 Gemini 请求。这样契约的源头只有一处:不是聊天记录里的口头约定,而是一份能跑测试的文档。
常见问题 FAQ
只设 application/json 够不够?
探索阶段可以。只要代码读取固定字段,就同时提供 Schema。否则你得到的是「像 JSON 的散文」,不是接口。
Structured Output 能替代函数调用吗?
不能互相替代。函数调用是让模型选工具并填参数;Structured Output 是约束这一次回答的形状。要执行代码或打外部 API,用工具;只要一段类型化数据,用 Structured Output,少一圈往返。
为什么 Schema 被拒绝?
常见原因是用了当前端点不支持的关键词、递归过深、或把 responseSchema 与 responseJsonSchema 的方言混用。把 Schema 减到一个对象三个字段,确认能通,再往上加。
输出一定正确吗?
不一定。它保证形状合法,不保证事实。金额、邮箱、工单类别仍要规则或人工抽检。
总结与下一步
稳定拿到 JSON,靠的不是更长的「请只输出 JSON」,而是 MIME 类型加 Schema。扁平任务用responseSchema或responseJsonSchema都行;要$ref、联合类型或从 Pydantic/Zod 生成,走后者。拿到响应后仍要本地校验,并用格式化、Schema、Diff 把失败样例变成可回归的测试。
下一步:把你现在最容易碎的那一个接口,改成「一份 Schema、一次请求、一次本地 validate」。先让这一条安静下来,再复制到其他抽取任务。