前言
大多数人(包括我)之前肯定都没有听说过这样一个名词,JSON Schema,可能平时在开发中也没有使用过类似的东西
然而它其实早就默默应用在软件开发的各个方面中,最贴近我们前端的就是在 VS Code 中的配置项,就是通过 JSON Schema 定义的
那么,到底它是什么,为什么要使用它,应该怎么使用它(哲学三大拷问),接下来,我就简单的带大家了解一下
什么是 JSON Schema
大家都知道,JSON 是一种十分灵活的数据格式,然而,太过于灵活也就导致它用起来毫无章法,巨大的灵活性伴随着巨大的责任,因为同一个概念可以以多种方式表示。
例如:我们可以使用两种不同的 JSON 表达式表达一个人的基本信息
// 基本信息一
{
"name": "George Washington",
"birthday": "February 22, 1732",
"address": "Mount Vernon, Virginia, United States"
}
// 基本信息二
{
"first_name": "George",
"last_name": "Washington",
"birthday": "1732-02-22",
"address": {
"street_address": "3200 Mount Vernon Memorial Highway",
"city": "Mount Vernon",
"state": "Virginia",
"country": "United States"
}
}
显然,第二种要比第一种更加规整、正式,这里不是说孰对孰错,但是在某些场合下,我们需要对提供的 JSON 数据做一个定义,需要告诉别人哪些字段应该表示什么意思,防止对方做出不可预期的赋值,最简单的例子就是:通过 JSON 去做配置
在此场景下,JSON Schema 诞生了。
Schema 的含义
JSON Schema 是由两个单词组成的,JSON 大家都很熟悉了,而 Schema 在很多行业都有特指的术语,比如在数据库中,叫架构或者模式,而在这里,我认为和 XML Schema 类似: 是元数据的一个抽象集合,包含一套schema component: 主要是元素与属性的声明、复杂与简单数据类型的定义
以下 JSON Schema 片段描述了上面第二个示例的结构
{
"type": "object",
"properties": {
"first_name": { "type": "string" },
"last_name": { "type": "string" },
"birthday": { "type": "string", "format": "date" },
"address": {
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" },
"country": { "type" : "string" }
}
}
}
}
显然,JSON Schema 由 JSON 表达,它是用于“描述其他数据结构”的声明性格式,或者你也可以理解为通过 JSON 的定义,表达出了一种新的数据结构
JSON Schema 规范
以下的规范要么对一个关键字做解释,要么对一组行为做解释
声明数据类型
通过关键字 type
定义,type
是 JSON Schema 的基础,type
的值可以是一个字符串或字符串数组,可选的类型如下:
- string
- number
- integer
- object
- array
- boolean
- null
例子:
// 表示该字段为一个数字
{ type: number }
// 表示该字段为字符串或数字
{ "type": ["number", "string"] }
string 类型的其他字段
以下字段在
type: string
时才有用
minLength
最小长度,值为非负整数
maxLength
最大长度,值为非负整数
pattern
正则表达式,该正则语法是由 JavaScript 定义的,以下字段匹配一个简单的北美电话
{
"type": "string",
"pattern": "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$"
}
format
format 允许对常用的某些类型的字符串值进行基本语义验证,以下是部分规范中的指定格式,全部规范在这里
-
date-time 日期时间
-
date 日期
-
time 时间
-
email 邮件
-
hostname internet 主机名
-
ipv4 ipv4 地址
-
uri 通用资源标识符
-
regex 正则表达式
这里和
pattern
不一样,pattern
指的是这个字段验证方式,字段本身是一个普通字符串,而regex
指的是这个字段应该是一个正则
number 类型的其他字段
以下字段在
type: number
时才有用
multipleOf
将数字限制为给定数字的倍数,值为任何正数
minimum
最小值,值为非负整数
maximum
最大值,值为非负整数
object 类型的其他字段
以下字段在
type: object
时才有用
properties
对象的属性,值为一个对象,其中每个键是属性的名称,每个值是用于验证该属性的模式
例如:我们定义一个简单的地址对象如下
{
"type": "object",
"properties": {
"number": { "type": "number" },
"street_name": { "type": "string" },
"street_type": { "enum": ["Street", "Avenue", "Boulevard"] }
}
}
// 该地址符合定义
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" }
patternProperties
对象的属性,该值符合特定的正则表达式,例如:
// 以 S 开头的属性,为 string 类型
// 以 I 开头的属性,为 integer 类型
{
"type": "object",
"patternProperties": {
"^S_": { "type": "string" },
"^I_": { "type": "integer" }
}
}
// OK
{ "S_25": "This is a string" }
// not OK
{ "I_42": "This is a string" }
additionalProperties
是否允许额外的属性,也就是除了规定的属性外,是否允许自定义其他属性,例子如下:
{
"type": "object",
"properties": {
"number": { "type": "number" },
"street_name": { "type": "string" },
"street_type": { "enum": ["Street", "Avenue", "Boulevard"] }
},
"additionalProperties": false
}
// not OK,额外属性“direction”使对象无效
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "direction": "NW" }
另一种 additionalProperties 使用方式,允许你更详细的规定额外的属性
{
"type": "object",
"properties": {
"number": { "type": "number" },
"street_name": { "type": "string" },
"street_type": { "enum": ["Street", "Avenue", "Boulevard"] }
},
"additionalProperties": { "type": "string" }
}
required
定义所必须的对象属性,值为字符串数组,例如:
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"address": { "type": "string" },
"telephone": { "type": "string" }
},
"required": ["name", "email"]
}
propertyNames
如果有一些列属性名遵循特定约定,可使用该字段定义
// 规定属性名开头为字母
{
"type": "object",
"propertyNames": {
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
}
}
// OK
{
"_a_proper_token_001": "value"
}
// not OK
{
"001 invalid": "value"
}
minProperties
属性数量最小值,值为非负整数
maxProperties
属性数量最大值,值为非负整数
array 类型的其他字段
以下字段在
type: array
时才有用
items
定义数组中的每个字段,该值和 JSON Schema 规范的定义是一致的
在下面的例子中,我们定义数组中的每一项都是一个数字:
{
"type": "array",
"items": {
"type": "number"
}
}
[1, 2, 3, 4, 5] // OK
[1, 2, "3", 4, 5] // not OK,单个“非数字”会导致整个数组无效
也可以分别定义数组中的每个元素
{
"type": "array",
"items": [
{ "type": "number" },
{ "type": "string" },
{ "enum": ["Street", "Avenue", "Boulevard"] },
{ "enum": ["NW", "NE", "SW", "SE"] }
]
}
[1600, "Pennsylvania", "Avenue", "NW"] // OK
[24, "Sussex", "Drive"] // not OK,“Drive”不是可接受的街道类型之一
additionalItems
定义附加元素,同对象的 additionalProperties
字段类似
contains
定义数组的元素必须包含的类型,下面的例子定义数组必须包含数字:
{
"type": "array",
"contains": {
"type": "number"
}
}
minItems
数组长度最小值,值为非负整数
maxItems
数组长度最大值,值为非负整数
uniqueItems
限制每个元素是唯一的,值为 true | false
enum
该关键字用于将值限制为一组固定的值。它必须是一个包含至少一个元素的数组,其中每个元素都是唯一的,你可以类比他的值就是下拉框中的 options
例如:某个字段是字符串,它的值为几个颜色中的一个
{
"type": string,
"enum": ["red", "amber", "green"]
}
"red" // OK
"blue" // not OK
const
定义一个字段的值为一个常量
contentMediaType
定义该字段为的MIME类型,例如:mp4,mp3 等等,具体的 MIME 类型可参考
contentEncoding
定义该字段的编码类型,例如:binary, base64
Schema 组合
组合适用于多个字段相关的验证,例如:某个字段即可以是数字,也可以是字符串,那么就需要多个定义组合起来验证
allOf
字段对于所有 Schema 生效,也就是 &&
// 字段是字符串,长度也不超过 5
{
"allOf": [
{ "type": "string" },
{ "maxLength": 5 }
]
}
anyOf
字段满足任意一个或多个给定 Schema,也就是 ||
oneOf
字段满足一个 Schema 就可以
not
字段不能满足给定的 Schema,也就是 !
模式依赖
这种适用于多个字段关联的验证
dependentRequired
必要依赖,如果一个对象存在某个特定的属性,则另一个属性也必须存在,例如:
// 有 credit_card 字段时,必须有 billing_address 字段
{
"type": "object",
"properties": {
"name": { "type": "string" },
"credit_card": { "type": "number" },
"billing_address": { "type": "string" }
},
"required": ["name"],
"dependentRequired": {
"credit_card": ["billing_address"]
}
}
// OK
{
"name": "John Doe",
"credit_card": 5555555555555555,
"billing_address": "555 Debtor's Lane"
}
// not OK,这个实例有一个credit_card,但缺少一个billing_address。
{
"name": "John Doe",
"credit_card": 5555555555555555
}
dependenciesSchemas
定义某个字段存在时,另外的字段应该应用特定的 Schema,例如:
// 定义当 credit_card 字段存在时,billing_address 必填且为字符串
{
"type": "object",
"properties": {
"name": { "type": "string" },
"credit_card": { "type": "number" }
},
"required": ["name"],
"dependentSchemas": {
"credit_card": {
"properties": {
"billing_address": { "type": "string" }
},
"required": ["billing_address"]
}
}
}
条件语句
定义条件,使用 if
, then
和 else
关键字,关键字里的内容区域也是 JSON Schema
例如,假设您想编写一个模式来处理美国和加拿大的地址。这些国家/地区有不同的邮政编码格式,我们希望根据国家/地区选择要验证的格式。如果地址在美国,则该 postal_code 字段是“邮政编码”:五个数字后跟可选的四位后缀。如果地址在加拿大,则该 postal_code 字段是一个六位字母数字字符串,其中字母和数字交替出现。
{
"type": "object",
"properties": {
"street_address": {
"type": "string"
},
"country": {
"default": "United States of America",
"enum": ["United States of America", "Canada"]
}
},
"if": {
"properties": { "country": { "const": "United States of America" } }
},
"then": {
"properties": { "postal_code": { "pattern": "[0-9]{5}(-[0-9]{4})?" } }
},
"else": {
"properties": { "postal_code": { "pattern": "[A-Z][0-9][A-Z] [0-9][A-Z][0-9]" } }
}
}
// OK
{
"street_address": "1600 Pennsylvania Avenue NW",
"country": "United States of America",
"postal_code": "20500"
}
// not OK
{
"street_address": "24 Sussex Drive",
"country": "Canada",
"postal_code": "10000"
}
对于更复杂的情况,例如:如果有多个国家,那么条件模式可以和组合一起使用,例如:
{
"type": "object",
"properties": {
"street_address": {
"type": "string"
},
"country": {
"default": "United States of America",
"enum": ["United States of America", "Canada", "Netherlands"]
}
},
"allOf": [
{
"if": {
"properties": { "country": { "const": "United States of America" } }
},
"then": {
"properties": { "postal_code": { "pattern": "[0-9]{5}(-[0-9]{4})?" } }
}
},
{
"if": {
"properties": { "country": { "const": "Canada" } },
"required": ["country"]
},
"then": {
"properties": { "postal_code": { "pattern": "[A-Z][0-9][A-Z] [0-9][A-Z][0-9]" } }
}
},
{
"if": {
"properties": { "country": { "const": "Netherlands" } },
"required": ["country"]
},
"then": {
"properties": { "postal_code": { "pattern": "[0-9]{4} [A-Z]{2}" } }
}
}
]
}
模式识别
这里应用于 JSON Schema 与 JSON Schema 之间的继承,引用,在具体应用于继承和引用之前,我们先来认识以下如下的关键字
$id
定义 JSON Schema 的唯一标识,例如:你可以定义 $id 为网站的某个 URI
{ "$id": "http://yourdomain.com/schemas/myschema.json" }
JSON 指针
非 JSON Schema 关键字
一个标识子 JSON Schema 的路径 URI,例如:
https://example.com/schemas/address#/properties/street_address URI 标识了如下 JSON Schema 的 street_address
字段,那么在别的 JSON Schema 就可以通过该 URI 引用这个定义
{
"$id": "https://example.com/schemas/address",
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" }
},
"required": ["street_address", "city", "state"]
}
$anchor
这是另一种标识子 JSON Schema 的路径 URI 的方式,就是在字段位置使用锚点关键字
例如:https://example.com/schemas/address#street_address 标识了如下 JSON Schema 的 street_address
字段
{
"$id": "https://example.com/schemas/address",
"type": "object",
"properties": {
"street_address":{
"$anchor": "#street_address",
"type": "string"
},
"city": { "type": "string" },
"state": { "type": "string" }
},
"required": ["street_address", "city", "state"]}
接下来,我们可以定义 JSON Schema 之间的继承及引用
$schema
定义本 JSON Schema 的格式,你可以认为它定义了一个基类,例如:如下定义表明该 JSON Schema 遵循 https://json-schema.org/draft/2019-09/schema
规范
"$schema": "https://json-schema.org/draft/2019-09/schema"
$ref
定义引用的 JSON Schema 地址,$ref
的值是根据模式的Base URI解析的 URI 引用
例如:我们先定义一个 address
字段,再在其他定义中引用该字段定义
// address 定义
{
"$id": "https://example.com/schemas/address",
"type": "object",
"properties": {
"street_address":{ "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" }
},
"required": ["street_address", "city", "state"]
}
// 一个客户 Schema 的定义,其中的地址字段引用上面的定义
{
"$id": "https://example.com/schemas/customer",
"type": "object",
"properties": {
"first_name": { "type": "string" },
"last_name": { "type": "string" },
"shipping_address": { "$ref": "/schemas/address" },
"billing_address": { "$ref": "/schemas/address" }
},
"required": ["first_name", "last_name", "shipping_address", "billing_address"]
}
可以通过 #
来实现递归功能,例如:
{
"type": "object",
"properties": {
"name": { "type": "string" },
"children": {
"type": "array",
"items": { "$ref": "#" }
}
}
}
定义了如下数据格式
{
"name": "Elizabeth",
"children": [
{
"name": "Charles",
"children": [
{
"name": "William",
"children": [
{ "name": "George" },
{ "name": "Charlotte" }
]
},
{
"name": "Harry"
}
]
}
]
}
$defs
可以认为是局部变量,例如上面所说的 address ,如果只在本 JSON Schema 中大量使用,可以只用 $defs 定义在本 JSON Schema 中
{
"$id": "https://example.com/schemas/customer",
"type": "object",
"properties": {
"first_name": { "$ref": "#/$defs/name" },
"last_name": { "$ref": "#/$defs/name" },
"shipping_address": { "$ref": "/schemas/address" },
"billing_address": { "$ref": "/schemas/address" }
},
"required": ["first_name", "last_name", "shipping_address", "billing_address"],
// 局部定义
"$defs": {
"name": { "type": "string" }
}
}
你也可以通过 $defs
直接定义一整套复杂的局部 JSON Schema,这样引用的时候可以直接用定义的 $id
{
"$id": "https://example.com/schemas/customer",
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"first_name": { "type": "string" },
"last_name": { "type": "string" },
"shipping_address": { "$ref": "/schemas/address" },
"billing_address": { "$ref": "/schemas/address" }
},
"required": ["first_name", "last_name", "shipping_address", "billing_address"],
"$defs": {
"address": {
"$id": "/schemas/address",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "$ref": "#/definitions/state" }
},
"required": ["street_address", "city", "state"],
"definitions": {
"state": { "enum": ["CA", "NY", "... etc ..."] }
}
}
}
}
JSON Schema 实战
事实上,现如今流行的框架、库都有自己的 JSON Schema 配置,例如:package.json、tsconfig.json 等等,可以在 schemastore 找到熟知的 JSON Schema 定义,接下来我们挑几个一起看下
package.json
{
$schema: "http://json-schema.org/draft-04/schema#",
"definitions": {
......
"scriptsStart": {
"description": "Run by the 'npm start' command.",
"type": "string",
"x-intellij-language-injection": "Shell Script"
}
......
}
"properties": {
"name": {
"description": "The name of the package.",
"type": "string",
"maxLength": 214,
"minLength": 1,
"pattern": "^(?:@[a-z0-9-*~][a-z0-9-*._~]*/)?[a-z0-9-~][a-z0-9-._~]*$"
},
"keywords": {
"description": "This helps people discover your package as it's listed in 'npm search'.",
"type": "array",
"items": {
"type": "string"
}
},
"scripts": {
"description": "The 'scripts' member is an object hash of script commands that are run at various times in the lifecycle of your package. The key is the lifecycle event, and the value is the command to run at that point.",
"type": "object",
"properties": {
......
"start": {
"$ref": "#/definitions/scriptsStart"
}
}
}
......
}
}
根据定义,我们可以看到
- 遵循 JSON Schema draft-04 规范(当前已到 Draft 2020-12)
name
字段实际上是有一个正则校验的,虽然涵盖的范围比较广,比如:肯定不能空格开头,在此之前是不是还以为是所有字符串呢keywords
字段是一个字符串数组scripts
字段是一个对象,其中的start
属性,定义来自于局部定义/definitions/scriptsStart
,其中这个定义了该字段为一个字符串
这里看到
scriptsStart
还有个自定义关键字x-intellij-language-injection
,这个关键字应该是规范之外的关键字,但是 vscode 或者 Intellij 编辑器应该都适配了这个关键字,定义了该字段应该使用的语言
tsconfig.json
{
"$schema": "http://json-schema.org/draft-04/schema#",
"allOf": [
{
"$ref": "#/definitions/compilerOptionsDefinition"
}
......
],
"definitions": {
"compilerOptionsDefinition": {
"properties": {
"compilerOptions": {
"type": "object",
"description": "Instructs the TypeScript compiler how to compile .ts files.",
"properties": {
"baseUrl": {
"description": "Specify the base directory to resolve non-relative module names.",
"type": "string",
"markdownDescription": "Specify the base directory to resolve non-relative module names.\n\nSee more: https://www.typescriptlang.org/tsconfig#baseUrl"
},
"paths": {
"description": "Specify a set of entries that re-map imports to additional lookup locations.",
"type": "object",
"additionalProperties": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"description": "Path mapping to be computed relative to baseUrl option."
}
},
"markdownDescription": "Specify a set of entries that re-map imports to additional lookup locations.\n\nSee more: https://www.typescriptlang.org/tsconfig#paths"
},
}
}
}
}
}
}
- ts 的定义比较粗暴,他是由
allOf
组合与一系列definitions
定义组成的 - 我们看到常用的
compilerOptions
的定义,其中paths
字段是一个对象,每个对象的属性是一个数组,且该数组需保持元素唯一 - 同时他还是有自定义的关键字
markdownDescription
,应该表示一个 markdown 描述地址
最后
感谢大家看到这里,在我写这篇文章之前,我其实对 JSON Schema 的了解也只局限于一部分简单的定义,为什么我要了解他,实际上是做项目的过程中,有一些 JSON 数据过于灵活,不得不需要一些规范去定义他,然后才发现 JSON Schema 其实早已深入到我们写代码的方方面面。
在完成这篇分享的过程中,我又更加深入的了解了 JSON Schema,以及通过浏览目前流行的库的定义过程中,我对于他们定义的配置在文档之上又有了更进一步的认识,也希望这篇文章带给你们全新的认识,谢谢大家