schema校验

2023/1/28 schema

# 基础知识

# 定义一个 JSON Schema

$schema关键字来声明将架构写入哪个版本的 JSON 架构规范。

{ "$schema": "https://json-schema.org/draft/2020-12/schema" }

# 定义唯一标识符

{ "$id": "http://yourdomain.com/schemas/myschema.json" }

# 类型关键词

# string 字符串类型

{ "type": "string" }

检测

# 通过检测
"aaa"
"b"

# number 数字类型

{ "type": "number" }

检测

# 通过检测
11
11.1
# 未通过检测
"11"

# 多个类型

{ "type": ["number", "string"] }

检测

# 通过检测
42
"Life, the universe, and everything"
# 未通过检测
["Life", "the universe", "and everything"]
[1,"Life"]

# 字符串类型

# length 字符串长度

{
  "type": "string",
  "minLength": 2,
  "maxLength": 3
}

检测

# 通过检测
"AB"
"ABC"
# 未通过检测
"A"
"ABCD"

# 正则表达式方式

{
  "type": "string",
  "pattern": "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$"
}

检测

# 通过检测
"555-1212"
"(888)555-1212"
# 未通过检测
"(888)555-1212 ext. 532"
"(800)FLOWERS"

# 格式化

JSON 没有日期类型,所以日期需要编码为字符串。

# 日期和时间

日期和时间在中表示。这是日期格式的子集,通常也称为 ISO8601 格式。

  • "date-time":日期和时间在一起,例如, 2018-11-13T20:20:39+00:00
  • "time": new in draft 7时间,例如,20:20:39+00:00
  • "date"草案 7 中的新日期,例如,2018-11-13
  • "duration"2019-09 草案中的新内容持续时间定义的持续时间。例如,P3D表示持续时间为 3 天。

# 电子邮件地址

# 主机名

# IP 地址

# 资源标识符

# URI 模板

  • "uri-template":根据 RFC6570 (opens new window)草案 6 A URI 模板(任何级别)中的新内容。如果您还不知道 URI 模板是什么,您可能不需要这个值。

# JSON 指针

# 正则表达式

# 数字类型

# integer 整数类型

{ "type": "integer" }

检测

# 通过检测
42
-1
1.0  # 小数部分为零的数字被视为整数
# 未通过检测
3.1415926
"42"

# number 数字类型

检测

# 通过检测
42
-1
1.0
3.1415926
# 未通过检测
"42"

# Multiples 倍数

将数字限制为给定数字的倍数

{
  "type": "number",
  "multipleOf": 10
}

检测

# 通过检测
0
10
20
100
# 未通过检测
0.1
23

# Range 范围

使用 minimummaximum关键字的组合来指定的(或者exclusiveMinimum用于 exclusiveMaximum表示排他范围)。

{
  "type": "number",
  "minimum": 0,
  "exclusiveMaximum": 100
}

小于minimum:

-1

minimum包含在内,因此 0 有效:

0
10
99

exclusiveMaximum是独占的,所以 100 无效:

100

大于maximum

101

# 对象类型

{ "type": "object" }
// 通过检测
{
   "key": "value",
   "another_key": "another_value"
}

{
    "Sun": 1.9891e30,
    "Jupiter": 1.8986e27,
    "Saturn": 5.6846e26,
    "Neptune": 10.243e25,
    "Uranus": 8.6810e25,
    "Earth": 5.9736e24,
    "Venus": 4.8685e24,
    "Mars": 6.4185e23,
    "Mercury": 3.3022e23,
    "Moon": 7.349e22,
    "Pluto": 1.25e22
}

// 未通过检测
// 使用非字符串key是无效键

{
    0.01: "cm",
    1: "m",
    1000: "km"
}

"Not an object"

["An", "array", "not", "an", "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" }

number 是错误的类型是无效的

{ "number": "1600", "street_name": "Pennsylvania", "street_type": "Avenue" }

默认情况下,可以省略属性street_type 被省略

{ "number": 1600, "street_name": "Pennsylvania" }

默认情况下,所有的属性都省略也是有效的

{}

默认情况下,提供额外的属性是有效的:direction 额外属性

{
  "number": 1600,
  "street_name": "Pennsylvania",
  "street_type": "Avenue",
  "direction": "NW"
}

# Pattern Properties匹配属性

{
  "type": "object",
  "patternProperties": {
    "^S_": { "type": "string" },
    "^I_": { "type": "integer" }
  }
}

测试

# 测试通过
{ "S_25": "This is a string" }
{ "I_0": 42 }
{ "keyword": "value" } # 这是一个不匹配任何正则表达式的键:

# 测试未通过
{ "S_0": 42 } # 开头S_,则它必须是一个字符串
{ "I_42": "This is a string" } # 如果名称以 开头I_,则必须是整数

# Additional Properties 附加属性

默认情况下允许任何其他属性,设置additionalProperties 对额外属性进行约束

设置未false时不允许添加附加属性

{
  "type": "object",
  "properties": {
    "number": { "type": "number" },
    "street_name": { "type": "string" },
    "street_type": { "enum": ["Street", "Avenue", "Boulevard"] }
  },
  "additionalProperties": false
}

测试

# 通过测试
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" }

# 测试未通过
# direction 为额外属性不能通过测试
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "direction": "NW" }

可以使用非布尔模式对实例的附加属性施加更复杂的约束

{
  "type": "object",
  "properties": {
    "number": { "type": "number" },
    "street_name": { "type": "string" },
    "street_type": { "enum": ["Street", "Avenue", "Boulevard"] }
  },
  "additionalProperties": { "type": "string" }
}

测试

# 有效的:附加属性的值是一个字符串
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "direction": "NW" }

# 无效的:附加属性的值不是一个字符串
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "office_number": 201 }

# Extending Closed Schemas 扩展封闭模式

{
  "allOf": [
    {
      "type": "object",
      "properties": {
        "street_address": { "type": "string" },
        "city": { "type": "string" },
        "state": { "type": "string" }
      },
      "required": ["street_address", "city", "state"],
      "additionalProperties": false
    }
  ],

  "properties": {
    "type": { "enum": ["residential", "business"] }
  },
  "required": ["type"]
}

失败additionalProperties。“type”被认为是额外的。

{
   "street_address": "1600 Pennsylvania Avenue NW",
   "city": "Washington",
   "state": "DC",
   "type": "business"
}

失败required。“type”是必需的。

{
  "street_address": "1600 Pennsylvania Avenue NW",
  "city": "Washington",
  "state": "DC"
}

因为additionalProperties 在 allOf 里面认为,是不可以添加额外属性,require 又是必须的导致如何都出错。

解决方法:移至additionalProperties扩展架构并重新声明扩展架构中的属性

{
  "allOf": [
    {
      "type": "object",
      "properties": {
        "street_address": { "type": "string" },
        "city": { "type": "string" },
        "state": { "type": "string" }
      },
      "required": ["street_address", "city", "state"]
    }
  ],

  "properties": {
    "street_address": true,
    "city": true,
    "state": true,
    "type": { "enum": ["residential", "business"] }
  },
  "required": ["type"],
  "additionalProperties": false
}
# 测试通过
{
   "street_address": "1600 Pennsylvania Avenue NW",
   "city": "Washington",
   "state": "DC",
   "type": "business"
}

# 测试未通过
{
   "street_address": "1600 Pennsylvania Avenue NW",
   "city": "Washington",
   "state": "DC",
   "type": "business",
   "something that doesn't belong": "hi!"
}

# Unevaluated Properties 未评估的属性

unevaluatedProperties 除了它可以识别在子模式中声明的属性。

{
  "allOf": [
    {
      "type": "object",
      "properties": {
        "street_address": { "type": "string" },
        "city": { "type": "string" },
        "state": { "type": "string" }
      },
      "required": ["street_address", "city", "state"]
    }
  ],

  "properties": {
    "type": { "enum": ["residential", "business"] }
  },
  "required": ["type"],
  "unevaluatedProperties": false
}

测试

# 测试通过
{
   "street_address": "1600 Pennsylvania Avenue NW",
   "city": "Washington",
   "state": "DC",
   "type": "business"
}

# 测试未通过
{
   "street_address": "1600 Pennsylvania Avenue NW",
   "city": "Washington",
   "state": "DC",
   "type": "business",
   "something that doesn't belong": "hi!"
}

unevaluatedProperties允许你做更复杂的事情,比如有条件地添加属性。

{
  "type": "object",
  "properties": {
    "street_address": { "type": "string" },
    "city": { "type": "string" },
    "state": { "type": "string" },
    "type": { "enum": ["residential", "business"] }
  },
  "required": ["street_address", "city", "state", "type"],

  "if": {
    "type": "object",
    "properties": {
      "type": { "const": "business" }
    },
    "required": ["type"]
  },
  "then": {
    "properties": {
      "department": { "type": "string" }
    }
  },

  "unevaluatedProperties": false
}

如果type的类型为常量business,则设置departmentstring类型。

# 有效
{
  "street_address": "1600 Pennsylvania Avenue NW",
  "city": "Washington",
  "state": "DC",
  "type": "business",
  "department": "HR"
}

因为type的类型不是business,所以department应该为["street_address", "city", "state", "type"]一种。

# 无效
{
  "street_address": "1600 Pennsylvania Avenue NW",
  "city": "Washington",
  "state": "DC",
  "type": "residential",
  "department": "HR"
}

# Required Properties 必要属性

{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "email": { "type": "string" },
    "address": { "type": "string" },
    "telephone": { "type": "string" }
  },
  "required": ["name", "email"]
}

测试

# 有效

{
  "name": "William Shakespeare",
  "email": "bill@stratford-upon-avon.co.uk"
}

{
  "name": "William Shakespeare",
  "email": "bill@stratford-upon-avon.co.uk",
  "address": "Henley Street, Stratford-upon-Avon, Warwickshire, England",
  "authorship": "in question"
}

# 无效:缺少必要属性

{
  "name": "William Shakespeare",
  "address": "Henley Street, Stratford-upon-Avon, Warwickshire, England",
}

{
  "name": "William Shakespeare",
  "address": "Henley Street, Stratford-upon-Avon, Warwickshire, England",
  "email": null
}

# Property names 属性名称

检测 key 的有效性

{
  "type": "object",
  "propertyNames": {
    "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
  }
}

测试

# 有效
{
  "_a_proper_token_001": "value"
}

# 无效

{
  "001 invalid": "value"
}

# Size属性的多少

下图为,最少 2 个属性。最多 3 个属性。

{
  "type": "object",
  "minProperties": 2,
  "maxProperties": 3
}

测试

# 有效
{ "a": 0, "b": 1 }
{ "a": 0, "b": 1, "c": 2 }

# 无效
{}
{ "a": 0 }
{ "a": 0, "b": 1, "c": 2, "d": 3 }

# 数组类型

{ "type": "array" }

测试

# 有效
[1, 2, 3, 4, 5]
[3, "different", { "types" : "of values" }]

# 无效
{"Not": "an array"}

# Item 列表验证

{
  "type": "array",
  "items": {
    "type": "number"
  }
}

测试

# 有效
[1, 2, 3, 4, 5]
[]
# 无效
[1, 2, "3", 4, 5]

# Tuple validation 元组验证

{
  "type": "array",
  "prefixItems": [
    { "type": "number" },
    { "type": "string" },
    { "enum": ["Street", "Avenue", "Boulevard"] },
    { "enum": ["NW", "NE", "SW", "SE"] }
  ]
}

测试

# 有效
[1600, "Pennsylvania", "Avenue", "NW"]
[10, "Downing", "Street"]
[1600, "Pennsylvania", "Avenue", "NW", "Washington"]
# 无效
[24, "Sussex", "Drive"]  # Drive 不属于["Street", "Avenue", "Boulevard"]
["Palais de l'Élysée"]   # 缺少第一个number

# Additional Items 附加选项

{
  "type": "array",
  "prefixItems": [
    { "type": "number" },
    { "type": "string" },
    { "enum": ["Street", "Avenue", "Boulevard"] },
    { "enum": ["NW", "NE", "SW", "SE"] }
  ],
  "items": false
}

测试

# 有效
[1600, "Pennsylvania", "Avenue", "NW"]
[1600, "Pennsylvania", "Avenue"]
# 无效 Washington是无效的
[1600, "Pennsylvania", "Avenue", "NW", "Washington"]

允许附加值{ "type": "string" }

{
  "type": "array",
  "prefixItems": [
    { "type": "number" },
    { "type": "string" },
    { "enum": ["Street", "Avenue", "Boulevard"] },
    { "enum": ["NW", "NE", "SW", "SE"] }
  ],
  "items": { "type": "string" }
}

测试

# 有效
[1600, "Pennsylvania", "Avenue", "NW", "Washington"]
# 无效 20500不是字符串
[1600, "Pennsylvania", "Avenue", "NW", 20500]

# Contains 包含

items 要求每一个必须是同一个类型,contains 至少要包含一个或多个

{
  "type": "array",
  "contains": {
    "type": "number"
  }
}

测试

# 有效
["life", "universe", "everything", 42]
[1, 2, 3, 4, 5]
# 无效
["life", "universe", "everything", "forty-two"] # 缺少数字类型

# minContains / maxContains 最少最多包含

{
  "type": "array",
  "contains": {
    "type": "number"
  },
  "minContains": 2,
  "maxContains": 3
}

测试

# 有效
["apple", "orange", 2, 4]
["apple", "orange", 2, 4, 8]
# 无效
["apple", "orange", 2]  # 数字类型最少2个
["apple", "orange", 2, 4, 8, 16] # 数字类型最多3个

# Length 数组长度

{
  "type": "array",
  "minItems": 2,
  "maxItems": 3
}

测试

# 有效
[1, 2]
[1, 2, 3]
# 无效
[]  # 少于2
[1] # 少于2
[1, 2, 3, 4] # 大于3

# Uniqueness 唯一性

{
  "type": "array",
  "uniqueItems": true
}

测试

# 有效
[1, 2, 3, 4, 5]
[]
# 无效
[1, 2, 3, 3, 4] # 3出现了重复

# 通用关键字

# 枚举值

{
  "enum": ["red", "amber", "green"]
}

测试

# 有效
"red"
# 无效
"blue"

# 常数值

{
  "properties": {
    "country": {
      "const": "United States of America"
    }
  }
}

测试

# 有效
{ "country": "United States of America" }
# 无效
{ "country": "Canada" }

# 组合模式

# allOf

给定的数据必须对所有给定的子模式有效。

{
  "allOf": [{ "type": "string" }, { "maxLength": 5 }]
}

测试

# 有效
"short"
# 无效
"too long" # 超过最长的长度

# anyOf

必须满足任何一个或多个条件

{
  "anyOf": [
    { "type": "string", "maxLength": 5 },
    { "type": "number", "minimum": 0 }
  ]
}

测试

# 有效
"short"
12
# 无效
"too long" # 超过最长的长度
-5         # 最小值0

# oneOf

只能同时满足其中一个条件

{
  "oneOf": [
    { "type": "number", "multipleOf": 5 },
    { "type": "number", "multipleOf": 3 }
  ]
}

测试

# 有效
10
9
# 无效
2   # 不满足5的倍数和3的倍数
15  # 同时满足了5的倍数和3的倍数不行

# not

不能是这个类型

{ "not": { "type": "string" } }

测试

# 有效
42
{ "key": "value" }
# 无效
"I am a string" # 不能是字符串类型

# 带条件的子模式

# dependentRequired 依赖

使用关键字表示一个属性对另一个属性的依赖性

{
  "type": "object",

  "properties": {
    "name": { "type": "string" },
    "credit_card": { "type": "number" },
    "billing_address": { "type": "string" }
  },

  "required": ["name"],

  "dependentRequired": {
    "credit_card": ["billing_address"]
  }
}

测试

# 有效
{
  "name": "John Doe",
  "credit_card": 5555555555555555,
  "billing_address": "555 Debtor's Lane"
}

{
  "name": "John Doe"
}

{
  "name": "John Doe",
  "billing_address": "555 Debtor's Lane"
}

# 无效

# credit_card 依赖billing_address属性,但是没有billing_address属性
{
  "name": "John Doe",
  "credit_card": 5555555555555555
}

# dependentSchemas 依赖 schema

{
  "type": "object",

  "properties": {
    "name": { "type": "string" },
    "credit_card": { "type": "number" }
  },

  "required": ["name"],

  "dependentSchemas": {
    "credit_card": {
      "properties": {
        "billing_address": { "type": "string" }
      },
      "required": ["billing_address"]
    }
  }
}

测试

# 有效
{
  "name": "John Doe",
  "credit_card": 5555555555555555,
  "billing_address": "555 Debtor's Lane"
}

{
  "name": "John Doe",
  "billing_address": "555 Debtor's Lane"
}

# 无效

# 没有billing_address
{
  "name": "John Doe",
  "credit_card": 5555555555555555
}

# If-Then-Else 条件判断

{
  "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]" }
    }
  }
}

测试

# 有效
{
  "street_address": "1600 Pennsylvania Avenue NW",
  "country": "United States of America",
  "postal_code": "20500"
}

{
  "street_address": "1600 Pennsylvania Avenue NW",
  "postal_code": "20500"
}

{
  "street_address": "24 Sussex Drive",
  "country": "Canada",
  "postal_code": "K1M 1M4"
}

# 无效

# postal_code 应该为[A-Z][0-9][A-Z] [0-9][A-Z][0-9]
{
  "street_address": "24 Sussex Drive",
  "country": "Canada",
  "postal_code": "10000"
}

# 默认是 const值
{
  "street_address": "1600 Pennsylvania Avenue NW",
  "postal_code": "K1M 1M4"
}