← 返回博客首页

JSON Schema 实战:接口校验与类型生成的利器

从一个恼人的线上事故说起

前后端联调时最常踩的坑:后端认为 price 一定是数字,前端传入 "128.5"(字符串)或漏传 userId,直到运行时才爆出 500。这类问题靠人眼 review 拦不住,靠直觉也难测全——正确姿势是用 JSON Schema 把『这里必须是 number、这个必填、数组至少 1 个』提前声明成契约。

本篇文章带你从校验学起,再引申到类型生成。

一个最小 Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["id", "total"],
  "properties": {
    "id":   { "type": "string", "minLength": 1 },
    "total": { "type": "number", "minimum": 0 },
    "items": { "type": "array", "minItems": 1,
               "items": { "type": "object",
                          "properties": {
                            "sku": { "type": "string" },
                            "price": { "type": "number", "minimum": 0 }
                          }, "required": ["sku", "price"] } },
    "status": { "enum": ["paid", "pending", "cancelled"] }
  }
}

这份 Schema 表达了四条关键的『硬规则』:必填 id/totaltotal ≥ 0、items 非空数组且每个元素都含 sku+pricestatus 只能取三个值之一。校验器按它跑一遍,问题当场暴露。

核心关键字速查

关键字 用途
type object/array/string/number/integer/boolean/null
required 必填字段数组
properties 对象字段各自的 Schema
items / prefixItems 数组元素 Schema
enum 允许的取值集合
const 固定值(判别字段)
allOf/anyOf/oneOf 组合多份子 Schema
$ref 引用已定义 Schema(复用)
format / pattern 格式与正则约束

用严格性「收紧」生成的 Schema

从样本 JSON 自动生成(json-to-jsonschema)得到的是宽松推断,适合起步但不具备约束力。要让它真正可校验,人工加三件事:

  1. 显式 required:把确实必填的字段列全;
  2. 值域约束:字符串补 enum/formatdate-timeemail),数字补 minimum/maximum,数组补 minItems/maxItems
  3. 统一入口 $ref + $defs:把可复用对象抽成 $defs,多处 $ref 引用,避免 Schema 膨胀重复。

oneOf 实现类型判别

联调中常遇到『同一结构体按字段值不同、形状不同』的事件体,例如 notificationtype 区分 email/sms:

"oneOf": [
  { "properties": { "type": { "const": "email" }, "to": { "type": "string" } }, "required": ["to"] },
  { "properties": { "type": { "const": "sms" },   "phone": { "type": "string" } }, "required": ["phone"] }
]

oneOf 恰好命中一个,校验器能靠 const 判别字段精确匹配。

从 Schema 生成强类型

一旦 Schema 成为唯一事实源,剩下的就是「翻译成各语言类型」:

JSON 样例 ──json-to-jsonschema──▶ Schema ──schema-to-typescript──▶ interface 定义
                                          ──schema-to-go──▶       struct 定义
  • 从 JSON → 类型:快,但与结构约束脱节,样本缺字段就漏类型;
  • 从 Schema → 类型:保留 enum/required/nullable 语义,产出更可靠,且校验与类型同源。

把整套接到你手头的 JSON 工具站里:格式化 → 生成 Schema → 收紧 → 生成 DTO,一次成型。

落地清单

  • [ ] 为每个接口定义一份 Schema,保存为版本化文件;
  • [ ] 用 json-to-jsonschema 起步,再人工补 required/constraints;
  • [ ] 运行时校验(后端或 API 网关挂上校验器)与类型生成同源;
  • [ ] 判别联合用 oneOf + const,公共对象用 $ref 复用;
  • [ ] 契约定好后,用生成器产出 TypeScript/Go DTO 替代手写。

自查

拿本地一个真实接口的 JSON 响应,走一遍「格式化 → 生成 Schema → 补 required → 生成 TS 类型」的完整流程,确认最后生成的类型能让你在编译器里直接发现「漏传字段」的隐患——这一步走通,JSON Schema 的价值你就抓住了。

常见问题

JSON Schema 主要是用来做什么的?

它是描述 JSON 数据「长得该像什么样」的标准:规定某个节点必须是 number、数组必须至少 1 个元素、某个字段必填、值必须来自 enum 等。典型用途:接口请求/响应的**运行时校验**、前端表单校验、文档化数据契约,以及**生成强类型定义**(TypeScript/Go/Jsonschema 派生类型)。本质是把「结构 + 约束」从代码里抽出来,变成可复用的、跨语言共享的声明文件。

怎么把 JSON 样例变成可以校验的 Schema?

最省力的方式是用**从 JSON 生成 Schema** 的工具(多数内置在 JSON 工具站里,如 json-to-jsonschema)。它会根据样本自动推断每个字段的类型、是否数组、嵌套结构。生成后建议人工收紧:给字符串加 enum 或格式约束(format: date-time/email)、明确的 required 列表、数组的 minItems、数字的 minimum——这样从『宽松推断』变成『严格契约』,校验才有意义。

oneOf / anyOf / allOf 分别用在什么场景?

三者都描述『多份子 Schema 如何组合』:**allOf** 要求同时满足所有子 Schema(叠加约束,常用于给基础对象加扩展);**anyOf** 满足任意一个即可(宽松多型,如『要么是字符串要么是 null』);**oneOf** **恰好**满足其中一个(严格多型,常用于判别联合类型,如按 activity.type 区分不同事件体)。判别联合推荐 oneOf + 在子 Schema 里用 const 固定判别字段,校验器能据此精确匹配。

从 Schema 生成模型和从 JSON 生成,有什么区别?

两者出发点不同:**JSON → 类型** 是从『实例』推导,快但只反映一份样本的形态,碰不到没出现的字段与约束;**Schema → 类型(json-schema-to-typescript 等)** 是从『契约』推导,能保留 enum、required、nullable 等语义,生成更干净、可读、带上 JSDoc 的类型。生产建议:用 Schema 作为唯一事实源,用它同时驱动运行校验和类型生成,避免『接口文档』与『DTO』两处维护。

← 返回博客首页