巢狀結構定義的麻煩事

OpenAPI Specification(OAS) 是描述 RESTful API 介面的標準規範,與程式語言無關(過去稱為「Swagger」)。它是自動產生文件、產生客戶端 SDK、建立 Mock 伺服器的基礎。

手動撰寫請求/回應主體的「Schema」——把層層嵌套的物件對應到 typepropertiesrequireditems——既耗時又容易出錯。

JSON 轉 OpenAPI 生成工具 只要輸入一份範例 JSON payload,就能自動產生 components/schemas 定義。

從 JSON 生成 OpenAPI 結構定義 | 支援 Swagger

型別推導的原理

產生器會逐一走訪 JSON 的每個值,將其對應到 OpenAPI 型別。

private static inferSchema(value: unknown): OpenApiSchema {
    if (value === null) return { type: 'null' };

    if (Array.isArray(value)) {
        // 若所有元素共用同一個 schema 就直接使用,否則包成 oneOf
        const itemSchemas = this.mergeSchemas(value.map((item) => this.inferSchema(item)));
        const items = itemSchemas.length === 1 ? itemSchemas[0] : { oneOf: itemSchemas };
        return { type: 'array', items };
    }

    if (typeof value === 'number') {
        return { type: Number.isInteger(value) ? 'integer' : 'number' };  // 區分整數與浮點數
    }
    // 物件會遞迴展開成 properties,且所有存在的鍵都視為必要欄位
}

這裡有兩個重點:

  1. 數字用 Number.isInteger 分類——30 會變成 integer30.5 則變成 number
  2. 陣列中若混雜不同型別,會轉成 oneOf。像 ["admin", 1, true] 這樣的陣列,會產生 items: { oneOf: [{type: string}, {type: integer}, {type: boolean}] }

日期字串的自動偵測

符合 ISO 8601 格式的字串,會用正規表示式比對,自動加上 format: date-time

if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/.test(stringValue)) {
    return { type: 'string', format: 'date-time' };
}

2026-01-15T12:00:00Z 這樣的值會被偵測到,但只有日期的字串(2026-01-15)或帶時區偏移的值(+09:00)不符合這個正規表示式,因此生成後需要手動補上 format: date

使用方法與輸出範例

{
  "id": 123,
  "name": "Yamada Taro",
  "email": "taro@example.com",
  "roles": ["admin", "editor"],
  "profile": {
    "createdAt": "2026-01-15T12:00:00Z",
    "isActive": true
  }
}

從這份 JSON 可以推導出:idintegernameemailstringrolesarrayprofile 為嵌套的 objectcreatedAt 則是帶有 format: date-timestring

  1. 執行 API,取得實際的 JSON 資料
  2. 貼到 JSON 轉 OpenAPI 生成工具
  3. 幾秒內就能得到 components/schemas 格式的定義
  4. 把生成的定義整合進您的 openapi.yaml

只要存在的鍵就會變成「必要」——這是刻意的設計

在目前的實作中,範例 JSON 裡存在的鍵,全部都會被標記為 required

for (const [key, val] of Object.entries(value)) {
    properties[key] = this.inferSchema(val);
    required.push(key);   // 只要存在的鍵,一律視為必要
}

這是刻意簡化的結果:光憑一份範例 JSON,沒辦法判斷某個鍵究竟是真的必要,還是那次剛好有值而已。如果您實際的 API 有可省略的欄位,請在生成後手動把它們從 required 中移除。

生成後務必檢查的項目

從 JSON 產生的 OpenAPI 結構定義,應該視為「精確度較高的草稿」,而非最終定案。

  • 把實際上可以省略的欄位從 required 中移除
  • 確認 ID、金額、數量等數值欄位該用 integer 還是 number
  • 確認日期字串是否加上了您期望的 format(自動偵測只涵蓋 date-time
  • 檢查陣列是否只採樣到空陣列(空陣列會變成 items: {}
  • 補上 descriptionexample,讓使用者理解各欄位的意義

遵循 OpenAPI 3.1

產生的結構定義以 OpenAPI 3.1.0 為目標。不同於 OpenAPI 3.0 的 nullable: true,這裡採用 3.1 標準化的 type: "null"(或聯合型別)寫法。如果您既有的專案是統一使用 OpenAPI 3.0,匯入輸出結果前可能需要轉換這個差異。

常見問題

也能用在 Swagger 嗎?

可以。OpenAPI 過去曾稱為 Swagger,現在仍常聽到「Swagger schema」「Swagger UI」這類說法。生成的結構定義可以直接用在 Swagger UI 等文件工具上。

請求主體與回應主體都能用嗎?

可以。POST/PUT 請求的 JSON、GET 回應的 JSON、Webhook 的 payload,只要是結構明確的 JSON,都能當作結構定義的起點。

陣列中的型別不一致時會怎麼處理?

會用 oneOf 把各種不同的型別列出來。至於要統一型別,還是就維持 oneOf 的形式,屬於 API 設計上的判斷。

需要在意 OpenAPI 3.0 與 3.1 的差異嗎?

如果您既有的專案統一使用 OpenAPI 3.0,匯入輸出結果前,請先確認 nullabletype: null 的處理方式。若是新專案或內部工具用途,直接採用 OpenAPI 3.1 的表示法通常沒有問題。

總結

  • 型別推導是遞迴走訪每個值:數字分成整數/浮點數,陣列中型別混雜會轉成 oneOf
  • 日期偵測以正規表示式為基礎,只涵蓋 date-time 格式,其餘需要手動調整
  • 範例中存在的鍵一律會變成 required,可省略的欄位請在生成後自行移除
  • 輸出以 OpenAPI 3.1.0 為準

自動生成終究只是草稿。生成後補上 descriptionexample,才能變成團隊成員與 API 使用者都樂於閱讀的文件。