本文 AI 產出,尚未審核

JSON Schema 轉 TypeScript 型別

TypeScript 實務開發與架構應用


簡介

在前端與後端的資料交換過程中,JSON Schema 常被用來描述 API 輸入/輸出、資料庫文件或設定檔的結構。它提供了機器可讀的驗證規則,讓開發團隊可以在編譯階段或執行時自動檢查資料的正確性。
然而,前端開發者在撰寫 TypeScript 程式碼時,往往還是需要手動寫出對應的介面 (interface) 或型別 (type) 以取得型別安全。手動同步兩套定義既容易出錯,也會增加維護成本。

JSON Schema 自動轉換成 TypeScript 型別,可以一次解決以下兩個痛點:

  1. 型別一致性:Schema 更新時,TypeScript 型別同步更新,避免因手動遺漏而產生的執行時錯誤。
  2. 開發效率:開發者只需要關注業務邏輯,型別產生交給工具或程式碼自動化腳本,減少重複勞動。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握「JSON Schema 轉 TypeScript 型別」的技巧,讓你的專案在 型別安全可維護性 之間取得最佳平衡。


核心概念

1. JSON Schema 基礎

JSON Schema 是一套以 JSON 表示的結構描述語言,常見的關鍵字包括:

關鍵字 說明
type 資料型別,如 stringnumberobjectarray
properties 物件屬性的子 Schema
required 必填欄位名稱陣列
enum 限定值的列舉
anyOf / oneOf / allOf 組合型別的條件

小技巧:在設計 API 時,盡量使用 enumformat(如 date-time)等額外資訊,能讓後續產生的 TypeScript 型別更精確。

2. 為什麼要轉成 TypeScript 型別?

  • 編譯期檢查:TypeScript 能在編譯階段捕捉欄位遺漏、型別不匹配等問題。
  • IDE 補完:轉換後的型別會自動提供 IntelliSense,提升開發者的開發體驗。
  • 文件生成:型別本身即是一份自動產生的文件,減少額外的 API 文件維護工作。

3. 轉換流程概覽

  1. 取得 JSON Schema:可以是本地檔案、遠端 URL,或是 Swagger / OpenAPI 的部份。
  2. 使用轉換工具:常見工具有 json-schema-to-typescriptquicktypetypebox 等。
  3. 產生 TypeScript 型別檔:將產出的 .d.ts.ts 檔案加入專案,作為型別引用。
  4. 在程式中使用import 產生的型別,搭配 axiosfetch 等 HTTP 客戶端使用,得到完整的型別安全。

下面會以 json-schema-to-typescript 為例,示範完整的操作步驟與程式碼範例。


程式碼範例

前置作業:請先在專案根目錄安裝套件

npm install -D json-schema-to-typescript

範例 1:最簡單的物件轉換

JSON Schema (user.schema.json)

{
  "$id": "User",
  "type": "object",
  "properties": {
    "id":   { "type": "string" },
    "name": { "type": "string" },
    "age":  { "type": "integer", "minimum": 0 }
  },
  "required": ["id", "name"]
}

產生 TypeScript 型別

npx json2ts -i user.schema.json -o src/types/user.d.ts

產出的 user.d.ts

/**
 * This file was automatically generated by json-schema-to-typescript.
 * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
 * and run json2ts to regenerate this file.
 */

/**
 * User
 */
export interface User {
  id: string;
  name: string;
  /** Minimum: 0 */
  age?: number;
}

說明required 的欄位變成必填 (id, name),未列入 requiredage 變成可選 (age?)。


範例 2:列舉 (enum) 與字串格式 (format)

JSON Schema (order.schema.json)

{
  "$id": "Order",
  "type": "object",
  "properties": {
    "orderId":   { "type": "string", "format": "uuid" },
    "status":    { "type": "string", "enum": ["pending", "paid", "shipped", "canceled"] },
    "total":     { "type": "number", "minimum": 0 },
    "createdAt": { "type": "string", "format": "date-time" }
  },
  "required": ["orderId", "status", "total"]
}

產生型別

npx json2ts -i order.schema.json -o src/types/order.d.ts

產出的 order.d.ts

/**
 * Order
 */
export interface Order {
  /** format: uuid */
  orderId: string;
  /** enum: "pending" | "paid" | "shipped" | "canceled" */
  status: "pending" | "paid" | "shipped" | "canceled";
  /** Minimum: 0 */
  total: number;
  /** format: date-time */
  createdAt?: string;
}

重點enum 直接映射成 字面量聯合型別format 則保留註解供開發者參考,若想要更嚴格的型別,可自行建立 type UUID = string 並使用 type 替換。


範例 3:陣列與巢狀物件

JSON Schema (blog.schema.json)

{
  "$id": "BlogPost",
  "type": "object",
  "properties": {
    "title":   { "type": "string" },
    "author": {
      "type": "object",
      "properties": {
        "id":   { "type": "string" },
        "name": { "type": "string" }
      },
      "required": ["id"]
    },
    "tags": {
      "type": "array",
      "items": { "type": "string" }
    },
    "comments": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "userId": { "type": "string" },
          "content": { "type": "string" },
          "likes": { "type": "integer", "minimum": 0 }
        },
        "required": ["userId", "content"]
      }
    }
  },
  "required": ["title", "author"]
}

產生型別

npx json2ts -i blog.schema.json -o src/types/blog.d.ts

產出的 blog.d.ts

/**
 * BlogPost
 */
export interface BlogPost {
  title: string;
  author: {
    /** required */
    id: string;
    name?: string;
  };
  tags?: string[];
  comments?: {
    /** required */
    userId: string;
    /** required */
    content: string;
    likes?: number;
  }[];
}

說明

  • 陣列的 items 直接映射成 type[]
  • 巢狀物件會被內嵌於父介面中,若想要抽離成獨立型別,可在 Schema 中使用 $ref 引用外部檔案。

範例 4:使用 $ref 共享型別

address.schema.json

{
  "$id": "Address",
  "type": "object",
  "properties": {
    "street": { "type": "string" },
    "city":   { "type": "string" },
    "zip":    { "type": "string", "pattern": "^[0-9]{5}$" }
  },
  "required": ["street", "city"]
}

customer.schema.json

{
  "$id": "Customer",
  "type": "object",
  "properties": {
    "id":      { "type": "string" },
    "name":    { "type": "string" },
    "address": { "$ref": "address.schema.json#" }
  },
  "required": ["id", "name", "address"]
}

產生型別(一次產出全部)

npx json2ts -i address.schema.json -i customer.schema.json -o src/types/models.d.ts

產出的 models.d.ts(簡化)

export interface Address {
  street: string;
  city: string;
  zip?: string;
}

export interface Customer {
  id: string;
  name: string;
  address: Address;
}

實務提示:使用 $ref 可以避免型別重複定義,在大型專案中尤為重要。確保所有 Schema 都放在同一個目錄或設定 --cwd 參數,以正確解析相對路徑。


範例 5:結合 quicktype 產生更豐富的型別

json-schema-to-typescript 產出的型別偏向「最小可用」;若需要 自訂型別別名、函式簽名,可考慮 quicktype

npm install -D quicktype
npx quicktype -s schema -l ts -o src/types/product.d.ts product.schema.json

產生的型別會自動加入 type 別名、enumas const 等,適合希望在型別層面做更多控制的團隊。


常見陷阱與最佳實踐

陷阱 說明 最佳做法
Schema 與 Type 不同步 手動編寫型別後忘記更新 完全自動化產生;或在 CI 中加入 npm run typecheck:schema,確保產生的檔案與原始 Schema 同步
anyunknown 被產生 使用 additionalProperties: true 時,TS 會退化成索引簽名 Record<string, any> 明確列出允許的額外屬性,或使用 additionalProperties: false;必要時自行加上 Record<string, unknown>
enum 變成 string 若 Schema 中的 enum 使用了 type: ["string","null"],工具可能降級為 `string null` 而失去字面量
遞迴型別產生失敗 Schema 中有自引用(如樹狀結構)時,工具可能無法正確解析 使用 json-schema-to-typescript--enable-recursion 旗標,或改為 typebox 手寫遞迴型別
format 被忽略 format 僅是註解,TS 不會自動驗證 在程式碼中自行加入驗證函式(如 zodio-ts)或使用 ajv 搭配 ajv-formats 於執行時驗證

推薦的工作流程

  1. Schema 版本管理:將所有 JSON Schema 放在 schemas/ 目錄,使用 Git 追蹤。
  2. 自動產生腳本:在 package.json 加入
    "scripts": {
      "gen:types": "json2ts -i schemas/**/*.json -o src/types/schema.d.ts",
      "prebuild": "npm run gen:types"
    }
    
    讓每次編譯前自動產生最新型別。
  3. 型別檢查:在 CI 中加入 npm run lint && npm run test && npm run gen:types && git diff --exit-code,確保沒有未提交的型別變更。
  4. 執行時驗證:在 API 呼叫或訊息接收時,使用 ajv 以同一份 Schema 做驗證,保持 編譯期執行期 的一致性。

實際應用場景

1. 前端與後端共享模型

在微服務架構中,後端服務會以 OpenAPI (Swagger) 定義 API。前端團隊只要把 openapi.json 中的 components.schemas 匯出為 JSON Schema,使用上述腳本即能得到完整的 TypeScript 型別,直接在 axiosreact-query 的回傳型別上使用,減少手寫 DTO 的成本。

2. 事件驅動系統(Kafka / RabbitMQ)

每一筆訊息的 payload 常以 JSON Schema 描述。將 Schema 直接轉成 TypeScript 後,消費者在 consumer.on('message', (msg: MyEvent) => { … }) 中即可得到完整的屬性提示與錯誤檢查,避免因欄位變更導致的隱藏 bug。

3. 內部工具與設定檔

大型專案常有多份 JSON 設定檔(如 CI/CD pipeline、專案模板)。把這些設定的 Schema 轉成型別,讓開發者在編寫自訂化腳本時,IDE 能即時提示錯誤,提升開發效率與可靠度。

4. 程式碼產生器(Codegen)

某些團隊會根據資料模型自動產生 CRUD UI、表單驗證規則或 GraphQL schema。只要把 JSON Schema 作為唯一真相(single source of truth),所有產生的程式碼都會保持一致,降低版本衝突的風險。


總結

  • JSON Schema 為資料結構提供標準化、機器可讀的描述,TypeScript 則在編譯期提供型別安全。兩者結合,可讓前後端、服務與工具之間的資料契約保持同步。
  • 使用 json-schema-to-typescriptquicktypetypebox 等工具,能自動把 Schema 轉成 介面 / 型別,減少手動維護的負擔。
  • 注意 requiredenum$refadditionalProperties 等關鍵字的映射方式,避免產生過寬或過窄的型別。
  • 建議在 CI/CD 流程中加入型別產生與比對步驟,確保每一次 Schema 更新都會同步到 TypeScript,並配合 AJV 等執行時驗證庫,完成「編譯期 + 執行期」的雙重保護。

透過本文的概念與範例,你已掌握從 JSON SchemaTypeScript 型別 的完整流程。將這套方法落實於日常開發,能顯著提升程式碼品質、減少錯誤、加速開發速度,讓你的專案在快速迭代的同時,仍能保持高度的 型別安全可維護性。祝開發順利!