JSON Schema 生成器
概述
把自由格式的自然语言数据结构描述,转换为符合 JSON Schema Draft 2020-12 的可执行校验定义。核心动作有四件:抽取字段、推断类型与约束、组装 Schema、产出能通过该 Schema 校验的示例数据与人类可读的校验说明表。
判断标准只有一条:把生成的 Schema 喂给 Ajv / Python jsonschema,能编译通过;把示例数据喂进去,校验结果为 true。 任何一条不满足,输出即不合格。
适用场景
| 场景 | 典型说法 | 交付重点 |
|---|---|---|
| 接口设计 | 「设计一个创建订单的请求体」 | 严格模式、必填边界清晰 |
| 表单校验 | 「注册表单:邮箱必填、密码至少 8 位」 | pattern、format、错误信息友好 |
| 配置文件 | 「数据库连接配置,含主从节点」 | 默认值、枚举、嵌套结构 |
| 数据交换 | 「物流运单的系统间交换格式」 | 版本字段、$id 稳定 |
| 反向整理 | 「把这个 JSON 示例 / 这张表变成 Schema」 | 结构还原完整、不漏字段 |
不适用:需要生成的是 OpenAPI 文档全文(只做其中 components.schemas 部分)、Protobuf / XML Schema(语义不同,需明说差异)、运行时校验器代码(只能给示例片段,不替代业务代码)。
工作流定义
按顺序执行五步,不跳步。每步都有明确的产出物,前一步产出是后一步的输入。
步骤 1 —— 解析描述,抽取字段
- 定顶层实体:判断是单个对象、对象数组(顶层
type: "array"),还是多个互不相关的实体(拆成多个 Schema,用$defs或分文件)。 - 抽字段清单:逐条列出字段,中文字段名转 camelCase 英文。
- 抓修饰词:必填/可选、长度、范围、唯一、默认值、格式、层级关系。
- 标歧义项:类型或归属无法确定的,先列入待确认清单,不擅自编造。
字段命名约定:
| 中文 | 转换结果 | 规则 |
|---|---|---|
| 用户名 | username |
沿用业界固定译法,不逐字直译 |
| 手机号 | phoneNumber |
复合词 camelCase |
| 创建时间 | createdAt |
时间点统一 -At 结尾 |
| 是否启用 | isEnabled |
布尔加 is / has / can 前缀 |
| 收货地址 | shippingAddress |
用行业术语,不用 address2 这类数字后缀 |
| 商品列表 | items |
数组用复数名词 |
步骤 2 —— 推断类型与约束
先按关键词定类型,再按修饰词挂约束。两个都做完才进入下一步。
类型推断优先级(自上而下匹配,命中即停):
| 描述关键词 | type |
附加关键字 |
|---|---|---|
| 邮箱 / email | string |
format: "email" |
| 链接 / URL / 头像地址 | string |
format: "uri" |
| 日期 | string |
format: "date" |
| 时间 / 时间戳 / 创建时间 | string |
format: "date-time" |
| IP 地址 | string |
format: "ipv4" 或 "ipv6" |
| UUID / 唯一标识 | string |
format: "uuid" |
| 年龄 / 数量 / 件数 / 库存 | integer |
minimum: 0,语义明确时补 maximum |
| 价格 / 金额 / 费率 | number |
minimum: 0;精度写进 description,不加 multipleOf: 0.01(原因见「常见坑」) |
| 是否 / 启用 / 开关 | boolean |
无 |
| 列表 / 标签 / 明细 | array |
必须给 items |
| 地址 / 配置 / 明细对象 | object |
递归处理子字段 |
| 枚举选项(「A/B/C」) | string |
enum: [...] |
约束映射(修饰词 → 关键字):
| 修饰词 | 关键字 |
|---|---|
| 必填 | 加入最近作用域的 required 数组 |
| 可选 | 不进 required |
| 至少 N 字 / N 个 | minLength: N / minItems: N(数值用 minimum) |
| 最多 N 字 / N 个 | maxLength: N / maxItems: N(数值用 maximum) |
| N 到 M 之间 | minimum: N, maximum: M |
| 不重复 / 唯一 | uniqueItems: true |
| 默认 X | default: X |
| 固定格式(如 ORD-...) | pattern: "..." |
| A 出现时 B 必填 | dependentRequired 或 if + then |
| 只允许出现一次(如主节点) | 数组 + contains + minContains: 1 / maxContains: 1 |
数值边界纪律:只有语义能给出硬边界时才写 maximum(如年龄 0–150、月份 1–12)。写不出依据的不写,宁可欠约束。
步骤 3 —— 组装 JSON Schema
顶层骨架:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/<entity-name>.json",
"title": "<实体标题(中文可)>",
"description": "<一句话说明这个结构承载什么业务数据>",
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
}
八条组装规则:
$schema固定https://json-schema.org/draft/2020-12/schema,不写 draft-07。$id=https://example.com/schemas/+ 实体名 kebab-case;同一批交付内$id不重复。- 每个字段(含子字段)必须有
description,写业务含义,不写「字段名」这种废话。 - 嵌套对象默认行内展开,保证 Schema 自包含;同一子结构出现两次以上才上提
$defs+$ref。 - 数组必须写
items,不能只写type: "array"。 enum字段在description里标注每个取值的含义。additionalProperties: false作为默认严格模式;只在需要承接未知扩展字段时才设true。required只写作用于当前层的字段名,不做跨层聚合。
步骤 4 —— 生成示例数据
示例数据的作用是证明 Schema 可被满足,不是随便凑一个 JSON。生成策略:
| 字段特征 | 取值策略 |
|---|---|
format: "email" |
"zhangsan@example.com" |
format: "uri" |
"https://example.com/avatar/8821.png" |
format: "date" |
"2026-04-16" |
format: "date-time" |
"2026-04-16T14:23:51+08:00" |
format: "uuid" |
"3f2b8c41-9d7e-4a55-b3c8-0e6f1a2d4b7e" |
有 enum |
取第一个值 |
有 pattern |
按正则构造一条真能匹配的串 |
string 普通 |
贴合语义的中文或真实英文取值 |
integer / number |
落在 minimum–maximum 内、精度符合 multipleOf |
boolean |
true |
array |
2–3 个元素,元素同样按上述规则生成 |
硬性要求:
- 必须自身通过自身 Schema 校验——尤其检查
pattern、multipleOf、maxLength、required四类最容易踩雷的关键字。 - 禁止占位垃圾值:
"test"、"foo"、"string"、"123"一律不允许。 - 数组元素之间要有差异(不同商品、不同标签),不要三个相同对象。
步骤 5 —— 输出校验说明
每次执行都以固定的三章结构输出,顺序不变:
## JSON Schema—— 一个 ```json 代码块## 示例数据—— 一个 ```json 代码块## 校验说明—— 一张 Markdown 表格
表格列固定为:
| 字段路径 | 类型 | 必填 | 约束 | 说明 |
|----------|------|------|------|------|
| customer.userId | string | 是 | — | 用户唯一 ID |
| items[].unitPrice | number | 是 | ≥ 0,两位小数 | 商品单价(元) |
字段路径用 . 表示层级、[] 表示数组元素。嵌套对象若子字段已逐条列出,父行写「见子字段」即可,不重复展开。
输入输出约束
输入
形式 A —— 自然语言段落
用户注册表单,包含用户名(必填,2-20 字符)、邮箱(必填)、密码(必填,至少 8 位)、
年龄(可选,0-150)、性别(男/女/保密)、兴趣标签(字符串数组,最多 10 个,唯一)
形式 B —— 字段清单
实体:用户注册
- username:string,必填,2-20 字符
- email:string,必填,email 格式
- password:string,必填,至少 8 位
形式 C —— 已有素材反向整理:一段示例 JSON、一张数据库表结构、一个 TypeScript interface、一段接口文档。此时逐字段对齐,不增不减,原文有歧义处标注而不猜测。
三种形式都允许中英混写。
信息不足时必须先问,只问最关键的两三个问题,不列问卷:
- 顶层是单个对象还是对象数组?
- 严格模式(多余字段报错)还是宽松模式?
- 有没有字段是「A 存在时 B 才必填」的联动关系?
输出
| 项 | 约束 |
|---|---|
| 规范版本 | Draft 2020-12,唯一允许的 $schema 值 |
| 字段命名 | camelCase 英文;enum 取值小写下划线(wechat_pay)或小写(male) |
description |
中文,写业务含义 |
| 代码块语言 | 一律 json |
| 输出章节 | Schema / 示例数据 / 校验说明,三章齐全 |
| 行数 | 单文件超过 200 行时,主动提示可拆分为多文件 + $ref |
示例模板
示例 1 —— 用户注册表单
输入
> 生成一个用户注册表单的 JSON Schema。字段:用户名(必填,2-20 字符)、邮箱(必填)、密码(必填,至少 8 位,需包含字母与数字)、年龄(可选,0-150)、性别(男/女/保密,默认保密)、兴趣标签(字符串数组,可选,最多 10 个且唯一)。
JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/user-registration.json",
"title": "用户注册表单",
"description": "用户注册时提交的表单数据,含登录凭据与偏好设置",
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "显示名称,同时作为登录名",
"minLength": 2,
"maxLength": 20
},
"email": {
"type": "string",
"description": "用于账号验证与通知的邮箱地址",
"format": "email"
},
"password": {
"type": "string",
"description": "登录密码,必须同时包含字母与数字",
"minLength": 8,
"pattern": "^(?=.*[A-Za-z])(?=.*\\d).+$"
},
"age": {
"type": "integer",
"description": "用户年龄,未填写时由系统按实名信息补全",
"minimum": 0,
"maximum": 150
},
"gender": {
"type": "string",
"description": "性别:male 男 / female 女 / undisclosed 保密",
"enum": ["male", "female", "undisclosed"],
"default": "undisclosed"
},
"interests": {
"type": "array",
"description": "兴趣标签,用于内容推荐冷启动",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 50
},
"maxItems": 10,
"uniqueItems": true
}
},
"required": ["username", "email", "password"],
"additionalProperties": false
}
示例数据
{
"username": "zhangsan2026",
"email": "zhangsan@example.com",
"password": "Secure8pass",
"age": 28,
"gender": "male",
"interests": ["编程", "摄影", "篮球"]
}
校验说明
| 字段路径 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| username | string | 是 | 2–20 字符 | 显示名称兼登录名 |
| string | 是 | email 格式 | 验证与通知邮箱 | |
| password | string | 是 | ≥ 8 位,含字母与数字 | 登录密码 |
| age | integer | 否 | 0–150 | 用户年龄 |
| gender | string | 否 | enum: male / female / undisclosed,默认 undisclosed | 性别 |
| interests | array\<string\> | 否 | 最多 10 项、不重复、每项 1–50 字符 | 兴趣标签 |
示例 2 —— 电商订单(含条件必填与嵌套数组)
输入
> 电商订单 Schema:订单号(必填,ORD-YYYYMMDD-6 位流水号)、创建时间(必填)、状态(待支付/已支付/已发货/已完成/已取消)、客户信息(userId + 昵称,必填)、商品列表(至少 1 项,每项含商品 ID、名称、单价、数量、小计)、收货地址(省/市/区 + 详细地址 + 邮编 + 收件人 + 手机号,必填)、支付信息(支付方式、交易号、支付时间,支付后必填)、金额明细(商品总额、运费、折扣、实付,全必填)、备注(可选,最多 200 字)。
JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/ecommerce-order.json",
"title": "电商订单",
"description": "订单全量结构,覆盖客户、商品、收货、支付与金额明细",
"type": "object",
"properties": {
"orderId": {
"type": "string",
"description": "订单号,业务规则为 ORD-YYYYMMDD-六位流水号",
"pattern": "^ORD-\\d{8}-\\d{6}$"
},
"createdAt": {
"type": "string",
"description": "订单创建时间,ISO 8601 带时区",
"format": "date-time"
},
"status": {
"type": "string",
"description": "订单状态:pending 待支付 / paid 已支付 / shipped 已发货 / completed 已完成 / cancelled 已取消",
"enum": ["pending", "paid", "shipped", "completed", "cancelled"]
},
"customer": {
"type": "object",
"description": "下单客户信息",
"properties": {
"userId": { "type": "string", "description": "用户唯一 ID" },
"nickname": { "type": "string", "description": "显示昵称", "maxLength": 30 }
},
"required": ["userId", "nickname"],
"additionalProperties": false
},
"items": {
"type": "array",
"description": "商品明细,至少一项",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"productId": { "type": "string", "description": "商品唯一 ID" },
"productName": { "type": "string", "description": "商品名称", "maxLength": 100 },
"unitPrice": { "type": "number", "description": "单价(元),两位小数,由业务层保证精度", "minimum": 0 },
"quantity": { "type": "integer", "description": "购买数量", "minimum": 1 },
"subtotal": { "type": "number", "description": "行小计(元),两位小数", "minimum": 0 }
},
"required": ["productId", "productName", "unitPrice", "quantity", "subtotal"],
"additionalProperties": false
}
},
"shippingAddress": {
"type": "object",
"description": "收货地址",
"properties": {
"province": { "type": "string", "description": "省份" },
"city": { "type": "string", "description": "城市" },
"district": { "type": "string", "description": "区或县" },
"detail": { "type": "string", "description": "详细地址含门牌号", "maxLength": 200 },
"zipCode": { "type": "string", "description": "邮政编码,六位数字", "pattern": "^\\d{6}$" },
"recipientName": { "type": "string", "description": "收件人姓名", "maxLength": 20 },
"phoneNumber": { "type": "string", "description": "收件人手机号,中国大陆 11 位", "pattern": "^1[3-9]\\d{9}$" }
},
"required": ["province", "city", "district", "detail", "recipientName", "phoneNumber"],
"additionalProperties": false
},
"payment": {
"type": "object",
"description": "支付信息,订单进入已支付状态后必须存在",
"properties": {
"method": {
"type": "string",
"description": "支付方式:wechat_pay 微信 / alipay 支付宝 / bank_card 银行卡 / credit_card 信用卡",
"enum": ["wechat_pay", "alipay", "bank_card", "credit_card"]
},
"transactionId": { "type": "string", "description": "支付渠道返回的交易号" },
"paidAt": { "type": "string", "description": "支付完成时间", "format": "date-time" }
},
"required": ["method", "transactionId", "paidAt"],
"additionalProperties": false
},
"pricing": {
"type": "object",
"description": "金额明细,单位元,统一保留两位小数",
"properties": {
"itemsTotal": { "type": "number", "description": "商品总额(元),两位小数", "minimum": 0 },
"shippingFee": { "type": "number", "description": "运费(元),两位小数", "minimum": 0 },
"discount": { "type": "number", "description": "优惠抵扣金额(元),两位小数", "minimum": 0 },
"totalPaid": { "type": "number", "description": "实付金额(元),两位小数", "minimum": 0 }
},
"required": ["itemsTotal", "shippingFee", "discount", "totalPaid"],
"additionalProperties": false
},
"remark": {
"type": "string",
"description": "买家备注,可空",
"maxLength": 200
}
},
"required": ["orderId", "createdAt", "status", "customer", "items", "shippingAddress", "pricing"],
"allOf": [
{
"if": {
"properties": { "status": { "enum": ["paid", "shipped", "completed"] } },
"required": ["status"]
},
"then": { "required": ["payment"] }
}
],
"additionalProperties": false
}
示例数据
{
"orderId": "ORD-20260416-000128",
"createdAt": "2026-04-16T14:23:51+08:00",
"status": "paid",
"customer": {
"userId": "U20230001",
"nickname": "小明"
},
"items": [
{
"productId": "SKU-8821",
"productName": "Cherry 红轴机械键盘 87 键",
"unitPrice": 459.00,
"quantity": 1,
"subtotal": 459.00
},
{
"productId": "SKU-3356",
"productName": "胡桃木桌面收纳架",
"unitPrice": 79.90,
"quantity": 2,
"subtotal": 159.80
}
],
"shippingAddress": {
"province": "广东省",
"city": "深圳市",
"district": "南山区",
"detail": "科技园南区 T3 栋 1205 室",
"zipCode": "518000",
"recipientName": "张明",
"phoneNumber": "13800138001"
},
"payment": {
"method": "wechat_pay",
"transactionId": "WX2026041614235100001",
"paidAt": "2026-04-16T14:25:03+08:00"
},
"pricing": {
"itemsTotal": 618.80,
"shippingFee": 0.00,
"discount": 30.00,
"totalPaid": 588.80
},
"remark": "键盘要黑色,周末送达"
}
校验说明
| 字段路径 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| orderId | string | 是 | ^ORD-\d{8}-\d{6}$ |
订单唯一编号 |
| createdAt | string | 是 | date-time | 创建时间 |
| status | string | 是 | enum: 5 种状态 | 订单状态 |
| customer.userId | string | 是 | — | 用户 ID |
| customer.nickname | string | 是 | ≤ 30 字符 | 显示昵称 |
| items[] | array | 是 | ≥ 1 项 | 商品明细 |
| items[].productId | string | 是 | — | 商品 ID |
| items[].productName | string | 是 | ≤ 100 字符 | 商品名称 |
| items[].unitPrice | number | 是 | ≥ 0,两位小数 | 单价(元) |
| items[].quantity | integer | 是 | ≥ 1 | 购买数量 |
| items[].subtotal | number | 是 | ≥ 0,两位小数 | 行小计(元) |
| shippingAddress.* | object | 是 | 见子字段 | 收货地址 |
| shippingAddress.zipCode | string | 是 | ^\d{6}$ |
邮编 |
| shippingAddress.phoneNumber | string | 是 | ^1[3-9]\d{9}$ |
收件手机号 |
| payment | object | 状态为 paid/shipped/completed 时必填 | 见子字段 | 支付信息 |
| pricing.totalPaid | number | 是 | ≥ 0,两位小数 | 实付金额 |
| remark | string | 否 | ≤ 200 字符 | 买家备注 |
边界情况处理
描述不全
| 情况 | 处理 |
|---|---|
| 未说明必填/可选 | 业务主键与核心字段默认必填,附加信息默认可选,并在说明表中标注「按默认推断」 |
| 未给长度限制 | 不写 minLength / maxLength,避免收起合法数据 |
| 未给数值范围 | 仅在语义有硬边界时补(年龄 0–150、月份 1–12、百分比 0–100) |
| 类型确实无法判断 | 提问。非交互场景下按字段名语义推断,并在输出末尾单列「待确认项」 |
| 未说明严格模式 | 默认 additionalProperties: false,并告知可通过改为 true 放开 |
核心原则:宁可欠约束,不可过约束。 过严的 Schema 会把合法数据挡在门外,这类 bug 上线后极难定位;宽松的 Schema 后续加关键字不会破坏既有数据。
特殊结构
| 描述 | 写法 |
|---|---|
| 文件 / 图片 / 附件 | string + format: "uri"(存 URL),或 contentEncoding: "base64" + contentMediaType |
| 字典 / 映射(键不定) | type: "object" + additionalProperties: { 值类型 } |
| 元组(固定位置) | prefixItems: [...] + items: false(Draft 2020-12 写法,不用旧版 items 数组) |
| 联合类型(字符串或数字) | anyOf: [{...}, {...}];互斥场景用 oneOf |
| 递归结构(树、多级评论) | 顶层加 "$id",子节点用 {"$ref": "#"} 自引用 |
| 至少一个、最多一个 | 数组 + contains + minContains / maxContains |
| 多字段互斥(三者选一) | oneOf 包裹各分支,每分支只 required 自己的字段 |
| 值依赖另一字段 | dependentRequired(必填联动)或 dependentSchemas(结构联动) |
| 废弃字段 | 保留定义,description 标注「已废弃,勿新增使用」,deprecated: true |
| 金额精度 | 默认 number + minimum: 0,精度靠业务层约束;要求强精度时改存整数分 + integer + minimum: 0,并在 description 注明单位为「分」 |
大型 Schema(30+ 字段或 4 层以上嵌套)
- 先给顶层骨架,复杂子结构先用
$ref占位并说明将展开。 - 再逐个展开子结构,每个子结构单独一节。
- 复用两次以上的结构提到
$defs,用"$ref": "#/$defs/<name>"引用。 - 超过 200 行时提示拆分为多文件,并给出文件清单与引用关系。
常见坑(生成后必查)
| 坑 | 症状 | 修法 |
|---|---|---|
pattern 未双重转义 |
\d 被 JSON 解析成非法转义,Schema 编译失败 |
JSON 字符串里写 \\d |
required 写在错误层级 |
子对象必填字段被塞进顶层 required |
required 只作用于所在对象层 |
数组漏 items |
校验通过但不约束元素,等于没校验 | 每个 array 都补 items |
enum 混大小写 |
实际数据 paid 对不上 Paid |
全小写,多词用下划线 |
multipleOf: 0.01 配浮点金额 |
618.80 因二进制无法精确表示 0.01,被 Python / Ajv 判为「不是 0.01 的倍数」 |
去掉 multipleOf,精度交给业务层;要强精度就改存整数分 integer |
| 示例数据过不了自己的 Schema | maxLength: 20 却给了 22 字符的昵称 |
逐字段回读上限再定值 |
$id 重复 |
同一批多个 Schema 用同一个 $id,$ref 解析错乱 |
每个实体独立 $id |
与工具链的对接
- Ajv(Node.js):需装
ajv-formats才识别format,import Ajv from "ajv"; import addFormats from "ajv-formats";,new Ajv({ allErrors: true, strict: true })。 - Python:
jsonschema库的Draft202012Validator,format校验需显式启用FormatChecker。 - OpenAPI 3.1:原生基于 Draft 2020-12,可直接放入
components.schemas;3.0 不支持,需降级改写。
配套脚本
scripts/validate_pair.py 把「示例数据能通过 Schema 校验」这条要求变成可机械执行的检查,避免肉眼漏看。
# 基本校验:Schema 本身合规 + 示例数据通过校验
python scripts/validate_pair.py order.schema.json order.sample.json
# 附带字段统计与 description 缺失提示
python scripts/validate_pair.py order.schema.json order.sample.json --stats
退出码:0 全部通过 / 1 校验不通过(逐条打印出错路径)/ 2 环境问题(缺 jsonschema 库、文件读不到、JSON 解析失败)。
执行时机:必须在把 Schema 与示例数据交付给用户之前跑一次。脚本报 not a multiple of 时,先怀疑 multipleOf 与浮点精度冲突;报 is too long 时,回头核对该字段的 maxLength 与生成取值。
完成前自检清单
输出前逐条过一遍,全部通过再交付:
- [ ]
$schema是https://json-schema.org/draft/2020-12/schema - [ ]
$id唯一且 kebab-case - [ ] 每个字段都有
description - [ ] 每个
array都有items - [ ] 每个
required只含本层字段名 - [ ] 所有
pattern中的反斜杠已写成\\ - [ ] 示例数据能通过该 Schema 校验
- [ ] 示例数据无
test/foo/string之类占位值 - [ ] 校验说明表覆盖全部字段(含嵌套)
- [ ] 三章齐全且顺序为 Schema → 示例数据 → 校验说明