教程
如何使用 OpenAPI 自動生成 JSON API 文檔、TypeScript 類型和 API Client?從 OpenAPI Schema 到代碼生成完整教程
手寫文檔、手寫類型、再手寫 fetch,三處各漂一版,比寫接口本身更貴。把 OpenAPI Schema 當合同,文檔、類型和 Client 都從同一份 JSON 長出來。
前後端最貴的往往不是實現 GET /invoices,而是旁邊那三份會各自過期的東西:給人看的 API 文檔、給編譯器看的 TypeScript 類型、給運行時看的 fetch 封裝。2026 年仍然有團隊在 Wiki 裏貼示例 JSON,在 types.ts 裏手寫同一份字段,再在 api.ts 裏拼 URL。OpenAPI Schema 的價值,是把這三件事收成一份可機器讀的合同。你維護 openapi.json(或 YAML),文檔站、類型文件和 Client 都從它生成;字段改一次,三處一起變。站內 MCP 與 JSON Schema 講的是工具入參;結構化輸出 講的是模型最終答覆。本文只講 HTTP JSON API:從 OpenAPI Schema 走到文檔、類型和 Client。規範原文見 OpenAPI 3.1。生成器以 openapi-typescript 爲主路徑。
一份 Schema,三份產物:文檔、類型、Client
先把目標說死。OpenAPI 文檔不是另一本 Markdown,而是同一份 spec 的渲染:Scalar、Swagger UI、Redoc 都只是閱讀器。TypeScript 類型不是另一份手寫接口,而是 paths 與 components.schemas 的投影。API Client 不是「再封裝一層 axios」,而是按 operationId 或路徑生成的、帶着請求體和響應類型的函數。三份產物如果有兩份是手寫的,Schema 就只是牆上的海報。
選型可以後做,合同必須先有。後端用 FastAPI、Nest、Spring、Go 的運行時註解導出 spec,或前端先寫 openapi.json 再倒逼實現,都可以。禁止的是:先寫 Client,再根據調用方「感覺」補文檔。那樣沒有單一真相,CI 也無法判斷誰對。Agent 若要把 REST 當工具,也應讀這份 OpenAPI,而不是再抄一份 MCP inputSchema——兩份合同會立刻 drift。分層見上文 MCP 文。
OpenAPI 3.1 把 components.schemas 更緊地對齊 JSON Schema 2020-12:可空用 type: ["string", "null"],不再依賴 3.0 的 nullable: true。這對「文檔裏寫可空、類型裏卻是必填」這類事故更友好。你在 JSONVue 裏看到的 Schema 形狀,和生成器喫進去的,應當是同一份對象,而不是導出時又轉一遍方言。
| 產物 | 從哪來 | 不要手寫什麼 |
|---|---|---|
| JSON API 文檔 | 同一份 OpenAPI JSON,用 Scalar / Swagger UI 渲染 | Wiki 裏的示例 JSON、過期截圖 |
| TypeScript 類型 | openapi-typescript 生成 paths / components | interface Invoice 與 spec 平行維護 |
| API Client / hooks | openapi-fetch、Orval 或 hey-api 生成調用函數 | 手拼 URL、手寫 as Invoice |
先寫對 OpenAPI JSON:paths、components、operationId
生成器能做的上界,就是 spec 的質量。最少要有:openapi 版本、info、paths、以及複用模型放進 components.schemas。每個操作給穩定的 operationId(如 listInvoices、createInvoice),Client 用它當函數名;路徑一改,id 別跟着漂。請求和成功響應都聲明 application/json,並 $ref 到組件,不要在每個 path 裏複製一份字段表。
錯誤響應也要進合同。400 / 401 / 404 / 422 各給 JSON Schema,Client 才能把失敗收成可辨的聯合類型,而不是一個 unknown。查詢參數、路徑參數寫成 parameters,不要塞進 body 又在文檔裏口頭約定。安全方案(Bearer、OIDC)寫在 securitySchemes,生成的 Client 才知道往哪放 token——鑑權協議本身見 OAuth 2.1,REST 面同樣適用「token 在頭,不在 JSON 信封」。
下面是一份最小可用的發票 API。它不是完整產品 spec,但夠讓文檔、類型和 Client 三條流水線同時轉起來。Invoice 只出現一次;list 與 create 都引用它。先在本地用 JSON 格式化確認能 parse,再用 Schema 工具看 $ref 有沒有斷。
{
"openapi": "3.1.1",
"info": {
"title": "Invoices API",
"version": "2026.09.18"
},
"paths": {
"/invoices": {
"get": {
"operationId": "listInvoices",
"parameters": [
{
"name": "status",
"in": "query",
"schema": {
"type": "string",
"enum": ["draft", "paid", "void"]
}
}
],
"responses": {
"200": {
"description": "Invoice list",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["items"],
"properties": {
"items": {
"type": "array",
"items": { "$ref": "#/components/schemas/Invoice" }
}
}
}
}
}
}
}
},
"post": {
"operationId": "createInvoice",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/InvoiceDraft" }
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Invoice" }
}
}
}
}
}
}
},
"components": {
"schemas": {
"InvoiceDraft": {
"type": "object",
"required": ["customerId", "amount"],
"properties": {
"customerId": { "type": "string" },
"amount": { "type": "integer", "minimum": 1 },
"note": { "type": ["string", "null"] }
}
},
"Invoice": {
"allOf": [
{ "$ref": "#/components/schemas/InvoiceDraft" },
{
"type": "object",
"required": ["id", "status"],
"properties": {
"id": { "type": "string" },
"status": {
"type": "string",
"enum": ["draft", "paid", "void"]
}
}
}
]
}
}
}
}
生成給人看的 JSON API 文檔
文檔站的正確形態是:構建時把 openapi.json 拷到靜態目錄,旁邊掛一個閱讀器。Scalar、Swagger UI、Redoc 都讀同一文件。不要把「文檔」理解成再寫一疊 Markdown,再在 CI 裏和 spec 對兩遍。人改文檔、機器改 spec,第二天就分叉。
閱讀器能做的,只限於 spec 裏寫過的:沒有 example 就沒有可複製的示例;沒有錯誤響應就沒有失敗說明;description 爲空,頁面上就只剩路徑。生成文檔之前,先把合同本身補全。預覽時用瀏覽器打開閱讀器即可,spec 文件本身用 JSONVue 看結構,比在巨大 YAML 裏找斷掉的 $ref 更快。
把文檔 URL 寫進 info.contact 或內部門戶,但不要再維護第二份「給產品看的字段表」。產品要看的枚舉和必填,應當已經在 Schema 的 enum / required 裏。兩份字段表是 drift 的經典入口。
生成 TypeScript 類型:openapi-typescript
類型生成走 openapi-typescript 就夠作爲默認:零運行時、喫 OpenAPI 3.0 與 3.1,輸出 paths 與 components。一條命令:npx openapi-typescript openapi.json -o src/api/schema.d.ts。遠程 spec 也可以,但生產 CI 應釘住倉庫裏的文件,避免構建去打會變的 URL。
import type { paths, components } from "./schema";
type Invoice = components["schemas"]["Invoice"];
type ListOk =
paths["/invoices"]["get"]["responses"][200]["content"]["application/json"];
export type { Invoice, ListOk };
用類型時按路徑索引,而不是再聲明 interface。paths["/invoices"]["get"]["responses"][200]["content"]["application/json"] 就是列表成功體。需要複用模型時取 components["schemas"]["Invoice"]。這樣路徑一改,編譯器會在調用方報錯,而不是讓你去搜三個 Invoice 定義。
類型不是運行時校驗。生成的 .d.ts 在瀏覽器裏不存在。網絡上仍可能來一份缺字段的 JSON;編譯通過只說明你按合同寫了 Client。需要運行時門衛,再用 Orval/Kubb 生成 Zod,或對關鍵響應做一次 Schema 校驗。不要把「TypeScript 綠了」寫成唯一質檢。
生成 API Client:openapi-fetch、Orval、hey-api
只要類型時,配 openapi-fetch:createClient<paths>({ baseUrl }) 之後 client.GET("/invoices", { params: { query } })。請求體、查詢、路徑參數都被類型卡住;你不會把 createInvoice 的 body 傳到 list。這是 2026 前端的薄默認:少文件、可控 fetch、沒有一倉庫生成代碼。
import createClient from "openapi-fetch";
import type { paths } from "./schema";
const client = createClient({
baseUrl: "https://api.example.com",
headers: { Authorization: "Bearer …" }
});
const { data, error } = await client.GET("/invoices", {
params: { query: { status: "paid" } }
});
const created = await client.POST("/invoices", {
body: { customerId: "cus_1", amount: 1999, note: null }
});
需要 React Query hooks、Zod、MSW mock 時,再上 Orval。它從同一份 spec 長出調用函數和鉤子,適合已經以 Query 爲數據層的應用。hey-api(@hey-api/openapi-ts)走 SDK 函數 + 攔截器,適合要 operationId 當方法名、又不必生成上千個文件的團隊。OpenAPI Generator 的 TypeScript-axios 模板仍能用,但輸出更重,新項目不必當首選。
無論哪條流水線,生成文件不要手改。要改行爲:改 spec、改生成器配置、或在手寫層包一層(超時、重試、租戶頭)。手改生成物等於放棄合同。Client 裏的 token 放 Authorization 頭,不要塞進 JSON body。
| 工具 | 你得到什麼 | 什麼時候選 |
|---|---|---|
| openapi-typescript + openapi-fetch | 類型 + 薄 fetch Client,接近零運行時 | 默認起點;要自己管緩存時 |
| Orval | 類型 + 函數 + Query hooks + 可選 Zod/MSW | 已經用 TanStack Query,要 mock 跟 spec 走 |
| hey-api / Kubb | SDK 方法名,或插件式多產物 | 要攔截器平臺,或一個 operation 一個文件 |
CI 防漂移,用 JSONVue 核契約
合同一變,生成物必須重跑。CI 裏:校驗 spec(Spectral 或紅帽 OpenAPI linter)→ 跑生成器 → git diff --exit-code 看生成目錄。有 diff 就失敗,逼提交者帶上新類型。不要只在筆記本上跑一次 codegen 然後忘掉。後端改了字段、前端類型還是上週的,是這類事故的標準形態。
npx --yes openapi-typescript openapi.json -o src/api/schema.d.ts
npx --yes @redocly/cli lint openapi.json
git diff --exit-code -- src/api/schema.d.ts
聯調時你會經手至少三份 JSON:OpenAPI spec 本身、某次 200 的 body、某次 422 的錯誤體。再加一份你準備餵給生成器的「精簡夾具」。它們職責不同,不要揉進一個文件做「通用校驗」。失敗樣本各留一條:斷掉的 $ref、缺 operationId、響應 content 未聲明 application/json、兩版 spec 只改了枚舉。
瀏覽器裏即可完成:JSON 格式化看 spec 能否 parse;JSON Schema 校驗核 components.schemas 與真實 body;JSON Diff對比兩版 OpenAPI 或「模型 arguments vs 下游 body」;數據不離開本機。延伸閱讀:MCP 與 JSON Schema、結構化輸出、以及用同一份字段表生成工具外殼。
常見問題 FAQ
spec 用 YAML 還是 JSON?
生成器兩種都喫。倉庫裏 YAML 對人類 diff 更友好;往瀏覽器和 JSONVue 送時用 JSON。CI 裏固定一種源文件,另一種只做構建產物,避免兩份源互相改。
能不能只生成類型、不生成 Client?
能,而且常常應該。openapi-typescript 只出 .d.ts。現有 axios 封裝可以逐步改成喫這些類型。等路徑和 body 都由類型兜住了,再換 openapi-fetch 或 Orval,比第一天生成三千個文件更可控。
新項目用 OpenAPI 3.0 還是 3.1?
新合同用 3.1,好跟 JSON Schema 2020-12 對齊,可空和 const 少一層方言。存量 3.0 不必強升;先確認你的生成器(openapi-typescript 7.x、Orval 8)聲明支持 3.1,再遷。Swagger 2.0 先升級到 3.x,再談 Client。
OpenAPI 和 MCP inputSchema 能共用一份嗎?
業務字段可以共用一份 canonical JSON Schema,再分別生成 OpenAPI requestBody 與 MCP inputSchema。不要把整份 OpenAPI 文件丟給 MCP Client 當工具清單——傳輸、鑑權、方法名都不是同一層。一份字段表、兩副外殼,比複製粘貼更不容易 drift。
總結與下一步
OpenAPI 代碼生成可以寫成一句:一份機器可讀的 Schema,同時長出人讀的 JSON API 文檔、編譯期 TypeScript 類型、以及帶類型的 Client。手寫其中任何一份,合同就不再是合同。
落地順序:先讓 openapi.json 能 parse、有 operationId、錯誤響應齊全;再掛文檔閱讀器;然後生成類型;最後才選 Client。用 JSONVue 把 spec、成功 body、失敗 body 留在本地。工具入參讀 MCP 與 JSON Schema;模型答覆讀結構化輸出。