JSON Schema 轉 TypeScript 型別
TypeScript 實務開發與架構應用
簡介
在前端與後端的資料交換過程中,JSON Schema 常被用來描述 API 輸入/輸出、資料庫文件或設定檔的結構。它提供了機器可讀的驗證規則,讓開發團隊可以在編譯階段或執行時自動檢查資料的正確性。
然而,前端開發者在撰寫 TypeScript 程式碼時,往往還是需要手動寫出對應的介面 (interface) 或型別 (type) 以取得型別安全。手動同步兩套定義既容易出錯,也會增加維護成本。
將 JSON Schema 自動轉換成 TypeScript 型別,可以一次解決以下兩個痛點:
- 型別一致性:Schema 更新時,TypeScript 型別同步更新,避免因手動遺漏而產生的執行時錯誤。
- 開發效率:開發者只需要關注業務邏輯,型別產生交給工具或程式碼自動化腳本,減少重複勞動。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握「JSON Schema 轉 TypeScript 型別」的技巧,讓你的專案在 型別安全 與 可維護性 之間取得最佳平衡。
核心概念
1. JSON Schema 基礎
JSON Schema 是一套以 JSON 表示的結構描述語言,常見的關鍵字包括:
| 關鍵字 | 說明 |
|---|---|
type |
資料型別,如 string、number、object、array |
properties |
物件屬性的子 Schema |
required |
必填欄位名稱陣列 |
enum |
限定值的列舉 |
anyOf / oneOf / allOf |
組合型別的條件 |
小技巧:在設計 API 時,盡量使用
enum、format(如date-time)等額外資訊,能讓後續產生的 TypeScript 型別更精確。
2. 為什麼要轉成 TypeScript 型別?
- 編譯期檢查:TypeScript 能在編譯階段捕捉欄位遺漏、型別不匹配等問題。
- IDE 補完:轉換後的型別會自動提供 IntelliSense,提升開發者的開發體驗。
- 文件生成:型別本身即是一份自動產生的文件,減少額外的 API 文件維護工作。
3. 轉換流程概覽
- 取得 JSON Schema:可以是本地檔案、遠端 URL,或是 Swagger / OpenAPI 的部份。
- 使用轉換工具:常見工具有
json-schema-to-typescript、quicktype、typebox等。 - 產生 TypeScript 型別檔:將產出的
.d.ts或.ts檔案加入專案,作為型別引用。 - 在程式中使用:
import產生的型別,搭配axios、fetch等 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),未列入required的age變成可選 (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 別名、enum 的 as const 等,適合希望在型別層面做更多控制的團隊。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳做法 |
|---|---|---|
| Schema 與 Type 不同步 | 手動編寫型別後忘記更新 | 完全自動化產生;或在 CI 中加入 npm run typecheck:schema,確保產生的檔案與原始 Schema 同步 |
any 或 unknown 被產生 |
使用 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 不會自動驗證 |
在程式碼中自行加入驗證函式(如 zod、io-ts)或使用 ajv 搭配 ajv-formats 於執行時驗證 |
推薦的工作流程
- Schema 版本管理:將所有 JSON Schema 放在
schemas/目錄,使用 Git 追蹤。 - 自動產生腳本:在
package.json加入
讓每次編譯前自動產生最新型別。"scripts": { "gen:types": "json2ts -i schemas/**/*.json -o src/types/schema.d.ts", "prebuild": "npm run gen:types" } - 型別檢查:在 CI 中加入
npm run lint && npm run test && npm run gen:types && git diff --exit-code,確保沒有未提交的型別變更。 - 執行時驗證:在 API 呼叫或訊息接收時,使用
ajv以同一份 Schema 做驗證,保持 編譯期 與 執行期 的一致性。
實際應用場景
1. 前端與後端共享模型
在微服務架構中,後端服務會以 OpenAPI (Swagger) 定義 API。前端團隊只要把 openapi.json 中的 components.schemas 匯出為 JSON Schema,使用上述腳本即能得到完整的 TypeScript 型別,直接在 axios 或 react-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-typescript、quicktype或typebox等工具,能自動把 Schema 轉成 介面 / 型別,減少手動維護的負擔。 - 注意 required、enum、$ref、additionalProperties 等關鍵字的映射方式,避免產生過寬或過窄的型別。
- 建議在 CI/CD 流程中加入型別產生與比對步驟,確保每一次 Schema 更新都會同步到 TypeScript,並配合 AJV 等執行時驗證庫,完成「編譯期 + 執行期」的雙重保護。
透過本文的概念與範例,你已掌握從 JSON Schema 到 TypeScript 型別 的完整流程。將這套方法落實於日常開發,能顯著提升程式碼品質、減少錯誤、加速開發速度,讓你的專案在快速迭代的同時,仍能保持高度的 型別安全 與 可維護性。祝開發順利!