跳至內容

JSON Schema 新手入門

實作導向的 JSON Schema 教學:型別、必填、約束、$ref、oneOf,附可直接複製貼上的範例與工具建議。

JSON 本身沒有 schema。兩份都合法的 JSON 文件可以完全長得不一樣——這 份彈性既是格式的優勢,也是生產上的最大風險。JSON Schema 就是描述 JSON 文件形狀的標準:可以機械地驗證、可以由它生成型別、可以給消費端 當文件用。

本文是實用入門:足以寫第一份 schema、做驗證、並了解涵蓋 95% 真實情境 的四五個運算子。

JSON Schema 是什麼

JSON Schema 本身就是一份 JSON 文件,描述「合法」JSON 文件的樣貌—— 哪些欄位必填、允許的型別、值的範圍、巢狀物件的形狀。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "email": { "type": "string", "format": "email" }
  },
  "required": ["id", "email"]
}

驗證器 是接受 schema 與文件、判定是否符合並列出不符原因的函式庫。 JavaScript 主流兩款是 ajv(快、完全符合規格)與 zod(TypeScript 原生、但有自己的 schema 語言;可用 zod-to-json-schema 在兩者間互轉)。

第一份 schema

實用 schema 最少需要 typepropertiesrequired

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age": { "type": "integer" }
  },
  "required": ["name"]
}

用 ajv 驗證:

import Ajv from "ajv";
const ajv = new Ajv();
const validate = ajv.compile(schema);

console.log(validate({ name: "Ada", age: 36 })); // true
console.log(validate({ age: 36 })); // false
console.log(validate.errors); // [{ instancePath: "", message: "must have required property 'name'" }]

七個核心型別:stringnumberintegerbooleanobjectarraynulltype 可以是單一字串,或字串陣列以同時允許多型別。

字串與數字約束

字串:

  • minLengthmaxLength——字元數(code unit,非 grapheme)。
  • pattern——必須符合的正規表示式。
  • format——具名格式,例如 emailuridate-timeuuid。在 ajv 中預設關閉,需加 ajv-formats 啟用。
{
  "type": "string",
  "minLength": 8,
  "maxLength": 64,
  "pattern": "^[A-Za-z0-9_]+$"
}

數字:

  • minimummaximum——含界值。
  • exclusiveMinimumexclusiveMaximum——不含界值。
  • multipleOf——必須為此數的倍數。
{
  "type": "number",
  "minimum": 0,
  "exclusiveMaximum": 100,
  "multipleOf": 0.01
}

陣列與巢狀物件

items 描述陣列元素,並可自由巢狀:

{
  "type": "object",
  "properties": {
    "tags": {
      "type": "array",
      "items": { "type": "string" },
      "minItems": 1,
      "uniqueItems": true
    },
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" }
      },
      "required": ["street", "city"]
    }
  }
}

重複使用的子 schema,用 $defs(舊稱 definitions)配合 $ref

{
  "$defs": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" }
      },
      "required": ["street", "city"]
    }
  },
  "type": "object",
  "properties": {
    "billing": { "$ref": "#/$defs/address" },
    "shipping": { "$ref": "#/$defs/address" }
  }
}

enumconst 與選擇運算子

封閉值集合用 enumconst

{
  "type": "string",
  "enum": ["pending", "active", "archived"]
}

「多選一形狀」(判別聯合)用 oneOfanyOf

{
  "oneOf": [
    {
      "type": "object",
      "properties": { "kind": { "const": "email" }, "address": { "type": "string", "format": "email" } },
      "required": ["kind", "address"]
    },
    {
      "type": "object",
      "properties": { "kind": { "const": "phone" }, "number": { "type": "string" } },
      "required": ["kind", "number"]
    }
  ]
}

oneOf 要求 恰好一個 schema 符合;anyOf 要求至少一個。請優先 選擇有判別欄位的 oneOf,錯誤訊息會清楚很多。

additionalProperties 與嚴格度

JSON Schema 預設允許 未列在 properties 內的額外欄位。這對 API 請求 body 通常是錯的——欄位名稱打錯應該回 400,而非靜默忽略。

設成 additionalProperties: false 即可變嚴:

{
  "type": "object",
  "properties": { "name": { "type": "string" } },
  "required": ["name"],
  "additionalProperties": false
}

對自己控制的雙端 payload,嚴格是正解。對需要無痛演進的公開 API, 可預設寬鬆、再依版本逐步加嚴。

實務驗證

伺服端 endpoint 的兩階段流程:

  1. JSON.parse 請求 body。
  2. 用 schema 驗證解析結果。
import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);

app.post("/users", (req, res) => {
  if (!validate(req.body)) {
    return res.status(400).json({ errors: validate.errors });
  }
  // req.body 已確認符合 schema
});

對於前端與隨手驗證,JSON 驗證工具 接受 schema 與文件,並指出失敗的斷言。

延伸閱讀