Understanding JSON Schema

json schema 在线校验器

译自:Understanding 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" }
      }
    }
  }
}

type字段

type字段可以是一个字符串或一个数组

  • 如果是一个字符串则表示基本数据类型,如:42、42.0

    { "type": "number" }
    
  • 如果是一个字符串,则表示数据可以是其中的任一基本类型,如:42"Life, the universe, and everything",但不能是结构化的数据类型,如:["Life", "the universe", "and everything"]

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

jsonschema的五种基本类型

string
{ "type": "string" }

可以表示的字符串如:"This is a string""""Déjà vu"(unicode字符)

length

用于限制字符串的长度

{
  "type": "string",
  "minLength": 2,
  "maxLength": 3
}
正则表达式

使用pattern字段设置正则表达式,具体参见官方说明

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

用于表示整数类型。需要注意的是,小数点的存在与否并不能判断它是一个整数还是浮点数,例如11.0都会被认为是整数,但3.1415926则是浮点数。

number

用于表示任意数字类型,即整数或浮点数

{ "type": "number" }
multiples

用于表示特定数字的倍数,如下可以是010、20,但23不是10的倍数,所以允许。

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

使用minimummaximum表示的数字范围(或使用exclusiveMinimumexclusiveMaximum表示独占范围)

  • xminimum
  • x > exclusiveMinimum
  • xmaximum
  • x < exclusiveMaximum

如下可以表示01099,但-1100101是错误的:

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

注意在JSON Schema Draft 4中exclusiveMinimumexclusiveMaximum的工作方式并不相同,它们表示一个boolean值,用于判断是否排除minimummaximum

if exclusiveMinimum is false, x ≥ minimum.
if exclusiveMinimum is true, x > minimum.
object

objects是JSON中的mapping类型,即将"keys"映射到"values","keys"必须是字符串,通常将每一对映射称为"属性"。

{ "type": "object" }

可以表达如下值:

{
   "key": "value",
   "another_key": "another_value"
}
properties

属性是object中使用properties关键字定义的key-value对。properties的值是一个对象,每个key的值作为一个property的名称,且每个值都用来校验该属性。任何与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": 1600, "street_name": "Pennsylvania" }
    
  • { }
    
  • { "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "direction": "NW" }
    

但不能匹配

  • { "number": "1600", "street_name": "Pennsylvania", "street_type": "Avenue" }
    
Pattern Properties

有时候期望对于某一类属性名称,匹配一个特定的模式,此时可以使用patternProperties:它使用正则表达式来进行模式匹配。如果一个属性的名称匹配到特定的正则表达式,则使用对于的模式来校验该属性的值。

如下表示使用S_开头的属性必须是字符串类型,而使用 I_ 开头的则必须是整数类型,并忽略不匹配正则表达式的属性。

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

上述表达式可以匹配

  • { "S_25": "This is a string" }
    
  • { "I_0": 42 }
    
  • { "keyword": "value" }
    

但不能匹配:

  • { "S_0": 42 }
    
  • { "I_42": "This is a string" }
    
Additional Properties

additionalProperties关键字用于控制不在properties关键字或不在patternProperties正则表达式列表中的属性。默认情况下允许这类properties。将additionalProperties设置为false表示不允许额外的属性。

如下表达式不允许{ "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": false
}

还可以使用非boolean对额外的属性增加更加复杂的限制。如下表示进允许类型为字符串的额外属性,此时可以允许{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "direction": "NW" },但不允许{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "office_number": 201 }

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

如下将additionalPropertiespropertiespatternProperties结合起来使用,例如{ "keyword": "value" }不匹配propertiespatternProperties,但它匹配了additionalProperties,因此允许该对象。

{
  "type": "object",
  "properties": {
    "builtin": { "type": "number" }
  },
  "patternProperties": {
    "^S_": { "type": "string" },
    "^I_": { "type": "integer" }
  },
  "additionalProperties": { "type": "string" }
}
扩展封闭模式

需要注意由于additionalProperties只能识别相同子模式的属性,因此可能会限制使用Schema Composition关键字进行扩展。例如下述表达式本意是要求对象中包含"street_address", "city", "state"和"type"这几个字段:

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

但对于下述对象,会因为将"type"认为是额外的属性,而无法通过additionalProperties的校验

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

但下述对象又由于缺少"type"而无法通过required的校验

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

由于additionalProperties只能识别相同子模式中的properties,它会将非"street_address", "city"和"state"的属性认为是额外的属性,一种解决方案是将additionalProperties转移到扩展的模式中,并在扩展的模式中重新定义属性

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

  "properties": {
    "street_address": true,//使用boolean表示必须出现该属性
    "city": true,
    "state": true,
    "type": { "enum": [ "residential", "business" ] }
  },
  "required": ["type"],
  "additionalProperties": false
}

draft 2019-09可以使用unevaluatedProperties关键字解决这种问题

Unevaluated Properties

unevaluatedProperties 关键字与additionalProperties类似的,但它可以识别子模式的属性。因此上述例子可以写为:

{
  "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"为"business"时允许"department"属性。

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

上述表达式允许:

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

不允许:

{
  "street_address": "1600 Pennsylvania Avenue NW",
  "city": "Washington",
  "state": "DC",
  "type": "residential",
  "department": "HR"
}
Required Properties

默认情况下,properties关键字中的属性不是必须的,但可以通过required关键字指定需要的属性。

required关键字可以指定0或多个字符串数组,每个字符串都必须唯一。如下表达式要求对象中有"name"和"email"属性。

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

注意上述表达式不允许如下对象,这是因为null的类型不是"string",而是"null":

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

New in draft 6

属性的名称可以根据模式进行验证,而不考虑它们的值。如下强制所有的名称必须是有效的ASCII 字符

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

由于对象的keys必须是字符串,这也意味着propertyNames的模式至少是:

{ "type": "string" }
size

properties的数目可以使用minPropertiesmaxProperties进行限制,值为整数。

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

表示一组有序的元组,数组中可以包含不同类型的元素。

{ "type": "array" }

允许:

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

JSON使用了两种数组方式:

  • List validation: 任意长度的数组,每个元素都使用相同的模式
  • Tuple validation: 固定长度的数组,每个元素都有可能使用不同的模式
item

List validation下使用item关键字来校验数组中的元素

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

如上表达式允许数组[1, 2, 3, 4, 5][],但不允许[1, 2, "3", 4, 5]

Tuple 校验

假设为了表达一个地址:"1600 Pennsylvania Avenue NW"。该地址是一个4元组[number, street_name, street_type, direction]

其中:

  • number: 地址号码,必须是数字
  • street_name: 街区名称,必须是字符串
  • street_type: 街区类型,必须来自一组固定的字符串值
  • direction: 城市象限,必须来自一组固定的字符串值

为了实现上述目的,需要使用prefixItems关键字,prefixItems表示一个数组,每个元素即一个模式,对应文档数组的相应索引,即第一个元素校验输入数组的第一个元素,第二个元素校验输入数组的第二个元素。

在 Draft 4 - 2019-09中,使用items关键字的另一种形式来进行元组验证。当items是一个多模式数组是,它的行为和prefixItems相同。

实现上述目的的表达式如下:

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

不允许:

  • ["Palais de l'Élysée"] //第一个元素不是数字
    
  • [24, "Sussex", "Drive"] //第三个元素不匹配
    
额外的元素

可以使用items关键字控制是否允许出现prefixItems中定义的元组之外的元素。

如果将items设置为false

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

将不允许:

  • [1600, "Pennsylvania", "Avenue", "NW", "Washington"] //包含额外元素
    

允许:

  • [1600, "Pennsylvania", "Avenue"]
    

可以使用非boolean的模式表示更复杂的限制,表示可以添加那些额外的元素:

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

此时允许:

  • [1600, "Pennsylvania", "Avenue", "NW", "Washington"]
    

但不允许:

  • [1600, "Pennsylvania", "Avenue", "NW", 20500] //20500不是字符串
    
Contains

New in draft 6

contains关键字要求数组中至少出现一个特定模式的元素

如下不允许出现["life", "universe", "everything", "forty-two"]这种不带任何数字的数组:

{
   "type": "array",
   "contains": {
     "type": "number"
   }
}
minContains / maxContains

New in draft 2019-09

contains关键字配合使用,限制contains的模式次数。

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

不允许:

  • ["apple", "orange", 2]
    
  • ["apple", "orange", 2, 4, 8, 16]
    
length

使用minItemsmaxItems限制数组的长度

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

uniqueItems设置为true,确保数组中元素的唯一性

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

将不允许:

  • [1, 2, 3, 3, 4]
    
boolean
{ "type": "boolean" }

需要注意truefalse要小写

null
{ "type": "null" }

需要注意的是,在JSON中null并不代表某些内容不存在

通用关键字
Annotations

JSON Schema中有一些关键字,这些关键字不用于校验,仅用于描述模式,这类"注释"关键字并不是必须的,但建议在实践中使用,由此可以实现模式的"自文档"。

titledescription关键字必须是字符串。

default关键字指定了默认值,该值不会填充验证过程中缺失的值。一些非验证的工具,如文档生成器或格式生成器会使用该值来提示用户如何使用一个值。

New in draft 6examples关键字提供了一组校验模式的例子,它并不用于校验,仅帮助读者解释模式的影响和目的。examples中不需要default,可以将default看作是另一个examples

New in draft 7:通常会在API上下文中使用boolean类型的readOnlywriteOnly关键字,前者表示不可修改某个值,当使用PUT请求修改值时,会响应400 Bad RequestwriteOnly表示可以设置值,但将保持隐藏状态,即可以通过PUT请求设置一个值,但在无法通过GET请求检索到该值。

New in draft 2019-09deprecated关键字用来表示未来将会移除该实例值。

{
  "title": "Match anything",
  "description": "This is a schema that matches anything.",
  "default": "Default value",
  "examples": [
    "Anything",
    4035
  ],
  "deprecated": true,
  "readOnly": true,
  "writeOnly": false
}
Comments

New in draft 7 $comment

$comment关键字用于给模式添加注释,该值必须是字符串。

Enumerated values

enum关键字用于指定一组固定的值。它必须是一个数组,且最少包含一个元素,每个元素都是唯一的。

{
  "enum": ["red", "amber", "green", null, 42]
}
Constant values

New in draft 6

const关键字用于指定单个值。如下例,将country限制为"United States of America",不允许出现其他值:

{
  "properties": {
    "country": {
      "const": "United States of America"
    }
  }
}
Media: 字符串编码的非JSON数据

JSON Schema中有一组关键字用于描述和选择性校验保存在JSON字符串中的非JSON数据。由于很难为所有媒体类型编写校验器,因此JSON 模式校验器不需要基于这些关键字验证JSON字符串的内容。但对于那些需要消费经过校验的JSON的应用来说非常有用。

contentMediaType

contentMediaType关键字用于指定字符串内容的MIME类型,参见 RFC 2046。IANA正式注册了一系列MIME类型,但具体支持的类型将取决于应用程序和操作系统。

{
  "type": "string",
  "contentMediaType": "text/html"
}

可以允许"<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head></head></html>"

contentEncoding

contentEncoding关键字指定保存内容的编码类型,参见RFC 2054, part 6.1RFC 4648

可接受的值为7bit, 8bit, binary, quoted-printable, base16, base32和 `base64,如果没有指定,则与JSON文档的编码相同。

通常的用法如下:

  • 如果编码的内容和JSON文档相同,则无需指定contentEncoding,按原样将内容包含在字符串中即可。包含基于文本的类型,如text/htmlapplication/xml
  • 如果内容是二进制,将contentEncoding设置为base64,并使用Base64进行编码,这类包含很多媒体类型,如image/png或音频类型,如audio/mpeg.
{
  "type": "string",
  "contentEncoding": "base64",
  "contentMediaType": "image/png"
}

可以允许"iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAA..."

模式组合

JSON Schema中有一些关键字可以用于将模式组合到一起。注意,这并意味着它们会组合来自多个文件或JSON树的模式(尽管这些功能有助于实现这一点),更多参见构建复杂模式。组合模式可能很简单,比如允许同时根据多个标准校验一个值。

这些关键字对应于众所周知的布尔代数概念,如AND、OR、XOR和NOT。你可以使用这些关键字来表达标准JSON Schema关键字无法表达的复杂限制。

这些关键字为:

  • allOf: (AND) 必须通过所有子模式的校验
  • anyOf: (OR) 必须通过任一个子模式的校验
  • oneOf: (XOR) 必须只能通过某一个子模式的校验
  • not: (NOT) 不能通过给定模式的校验
allOf
{
  "anyOf": [
    { "type": "string", "maxLength": 5 },
    { "type": "number", "minimum": 0 }
  ]
}

可以允许:

  • "short"
    
  • 12
    

不允许:

  • "too long"
    
  • -5
    
oneOf
{
  "oneOf": [
    { "type": "number", "multipleOf": 5 },
    { "type": "number", "multipleOf": 3 }
  ]
}

可以允许:

  • 10
    
  • 9
    

不允许:

  • 2
    
  • 15 // 同时是3和5的倍数
    
not
{ "not": { "type": "string" } }

允许:

  • 42
    
  • { "key": "value" }
    

不允许:

  • "I am a string"
    
模式组合的特点
不合逻辑的模式

如下组合是不符合逻辑的,因为数据不可能既是字符串又是数字:

{
  "allOf": [
    { "type": "string" },
    { "type": "number" }
  ]
}
分解模式

可以将"因子"放到子模式的公共部分之外,如下两种模式是等价的:

{
  "oneOf": [
    { "type": "number", "multipleOf": 5 },
    { "type": "number", "multipleOf": 3 }
  ]
}
{
   "type": "number",
   "oneOf": [
     { "multipleOf": 5 },
     { "multipleOf": 3 }
   ]
 }
子模式条件
dependentRequired

dependentRequired关键字要求当对象中出现给定的属性时,要求出现特定的属性。例如,如果你有信用卡号,则必须保证还有一个账单地址,发明之如果没有信用卡号,那么也不需要账单地址了。使用dependentRequired关键字可以表示一个属性对其他属性的依赖关系。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"]
  }
}

允许:

  • {
      "name": "John Doe",
      "credit_card": 5555555555555555,
      "billing_address": "555 Debtor's Lane"
    }
    
  • {
      "name": "John Doe" //没有提供credit_card,不产生依赖
    }
    
  • {
      "name": "John Doe",
      "billing_address": "555 Debtor's Lane" //billing_address并没有任何依赖
    }
    

不允许:

  • {
      "name": "John Doe",
      "credit_card": 5555555555555555
    }
    
dependentSchemas

dependentSchemas关键字要求当出现给定的属性时,应用特定的子模式。下面表示当出现credit_card时,要求出现billing_address,且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"]
    }
  }
}

允许:

  • {
      "name": "John Doe",
      "credit_card": 5555555555555555,
      "billing_address": "555 Debtor's Lane"
    }
    
  • {
      "name": "John Doe",
      "billing_address": "555 Debtor's Lane" //不存在credit_card
    }
    
If-Then-Else

New in draft 7,与编程语言中的if/then/else类似。

if then else whole schema
T T n/a T
T F n/a F
F n/a T T
F n/a F F
n/a n/a n/a T

例如,如果你想编写一个模式来处理United States 和Canada的地址,这两个国家的邮政编码格式不同,我们需要根据不同的国家来进行校验。如果地址在United States,则postal_code字段为zipcode:5位数字,后面跟4位可选的数字后缀。如果地址在Canada,则postal_code字段为6位字母数字串。

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

不允许:

  • {
      "street_address": "24 Sussex Drive",
      "country": "Canada",
      "postal_code": "10000"
    }
    
  • {
      "street_address": "1600 Pennsylvania Avenue NW",
      "postal_code": "K1M 1M4"
    }
    

上例中并没有要求出现"country"属性,因此如果未定义"country"属性,默认行为会将"postal_code"验证为美国邮政编码。“default”关键字没有效果(只作提示作用)

上述方式只能处理两个国家的情况,如果要处理多个国家,可以将多个ifthen成对包含到allOf中。

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

允许:

  • {
      "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"
    }
    
  • {
      "street_address": "Adriaan Goekooplaan",
      "country": "Netherlands",
      "postal_code": "2517 JX"
    }
    

不允许:

  • {
      "street_address": "24 Sussex Drive",
      "country": "Canada",
      "postal_code": "10000"
    }
    
  • {
      "street_address": "1600 Pennsylvania Avenue NW",
      "postal_code": "K1M 1M4"
    }
    

上述"if"模式中的required字段是必须的,如果没有该字段,则会将该模式作为默认模式执行。例如,对于如下语句:

{
  "street_address": "1600 Pennsylvania Avenue NW",
  "postal_code": "K1M 1M4"
}

如果按照上述表达式执行,结果为:

Message:JSON does not match all schemas from 'allOf'. Invalid schema indexes: 0.
Schema path: #/allOf
	Message:JSON does not match schema from 'then'.
	Schema path:#/allOf/0/then/then
		Message:String 'K1M 1M4' does not match regex pattern '[0-9]{5}(-[0-9]{4})?'.
		Schema path:#/allOf/0/then/properties/postal_code/pattern

如果去掉所有的required字段,则会将所有if模式作为默认模式进行匹配校验,结果如下:

Message:JSON does not match all schemas from 'allOf'. Invalid schema indexes: 0, 2.
Schema path:#/allOf
	Message:JSON does not match schema from 'then'.
	Schema path:#/allOf/2/then/then
		Message:String 'K1M 1M4' does not match regex pattern '[0-9]{4} [A-Z]{2}'.
		Schema path:#/allOf/2/then/properties/postal_code/pattern
	Message:JSON does not match schema from 'then'.
	Schema path:#/allOf/0/then/then
		Message:String 'K1M 1M4' does not match regex pattern '[0-9]{5}(-[0-9]{4})?'.
		Schema path:#/allOf/0/then/properties/postal_code/pattern
implication

可以使用模式组合关键字来表示"if-then"条件,

{
  "type": "object",
  "properties": {
    "restaurantType": { "enum": ["fast-food", "sit-down"] },
    "total": { "type": "number" },
    "tip": { "type": "number" }
  },
  "anyOf": [
    {
      "not": {
        "properties": { "restaurantType": { "const": "sit-down" } },
        "required": ["restaurantType"]
      }
    },
    { "required": ["tip"] }
  ]
}

允许:

  • {
      "restaurantType": "sit-down",
      "total": 16.99,
      "tip": 3.4
    }
    
  • {
      "restaurantType": "fast-food",
      "total": 6.99
    }
    
  • { "total": 5.25 }
    

不允许:

  • {
      "restaurantType": "sit-down", //不满足anyOf
      "total": 16.99
    }
    
声明一个Dialect

一个JSON Schema版本称为一个Dialect,Dialect表示用于评估模式的一组关键字和语义。每个发布的JSON Schama都是一个新的Dialect。

$schema

$schema关键字用于声明JSON Schema的dialect。$schema关键字的值也是模式的标识符,可用于根据$schema标识的dialect 验证模式是否有效。描述另一个模式的模式称为"meta-schema"。

$schema位于整个文档的根,它不适用于外部引用的($ref,$dynamicRef)文档。

  • Draft 4: http://json-schema.org/draft-04/schema#
  • Draft 6:http://json-schema.org/draft-06/schema#.
  • Draft 7:http://json-schema.org/draft-07/schema#.
  • Draft 2019-09:https://json-schema.org/draft/2019-09/schema.
Guidelines

可以使用Meta-data关键字提供帮助信息,因为这类字段并不会影响校验过程。

{
  "type": "object",
  "requiredProperties": {
    "foo": { "type": "string" }
  }
}

允许:

  • { "foo": "bar" }
    
  • { "foo": 42 } //无法识别requiredProperties字段
    

构造复杂的模式

本章介绍如何使用工具来重用和构造模式。

Schema Identification

与其他编程语言类似,如果将模式分为多个逻辑单元,那么就可以互相引用。为了引用一个模式,需要一种方式来标识一个模式,称为non-relative URIs。

标识并不是必须的,只有在需要引用时才会用到标识。无标识的模式称为"匿名模式"。

URI术语有时可能不直观。在本文件中,使用了以下定义。

  • URI [1]非相对 URI: 包含一个 scheme (https)的完整URL,可能包含一个URL片段 (#foo)。有时,本文档会使用"非相对URI"来明确说明不允许使用相对URI
  • relative reference [2]: 不包含 scheme (https)的部分URL,可能包含一个片段(#foo).
  • URI-reference [3]: 相对引用或非相对URI,可能包含一个URL片段 (#foo)
  • absolute URI [4] 包含一个 scheme (https)的完整URL,但不包含URL片段 (#foo)

虽然使用URL来标识模式,但但这些标识并不需要网络可达。

基本URI

使用非相对URI可能会很麻烦,因此JSON模式中使用的所有URI都可能是URI引用,它们会根据模式的基本URI进行解析,从而生成非相对URI。本节描述如何确定模式的基本URI。

RFC-3986中定义了基本URI和相对引用解析。

检索URI

用于获取模式的URI称为“检索URI”。

假设使用URI引用了一个模式https://example.com/schemas/address,然后检索到以下模式。

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

此时该模式的基本URI与检索URI相同

$id

可以在模式的根使用$id关键字定义基本URI,$id的值是一个URI引用,没有根据检索URI解析的片段。

假设URIs https://example.com/schema/addresshttps://example.com/schema/billing-address都使用了如下模式:

{
  "$id": "/schemas/address",

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

无论使用两个URI中的哪一个来检索此模式,基本URI都是https://example.com/schemas/address,这是$id根据检索URI解析出的结果。

然而,在设置基本URI时使用相对引用可能会有问题。例如,不能将此模式用作匿名模式,由于没有检索URI,且无法对任何内容解析相对引用。出于这种原因,建议在使用$id声明基本URI时,使用完整的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"]
}
JSON指针

除了表示一个模式文档,还可以标识子模式。最常见的方式是在指向该子模式的URI片段中使用JSON 指针

JSON指针描述了一个斜杠分隔的路径,用于遍历文档中对象中的键。/properties/street_address意味着:

  • 找到第一个键properties的值
  • 在该对象中找到键street_address的值

URI https://example.com/schemas/address#/properties/street_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"]
}
$anchor

一种不太常见的识别子模式的方法是使用$anchor关键字在模式中创建一个命名锚点,并在URI片段中使用该名称。锚点必须以字母开头,后跟任意数量的字母、数字-, _, :.

URI https://example.com/schemas/address#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"]
}

$ref

一个模式可以使用$ref关键字引用另一个模式。 $ref 是一个根据基本URI解析的URI引用。

假设需要定义一个客户记录,每个客户都可能有一个送货地址和账单地址。地址格式是相同的,都有一个街区地址、城市和国家。

$ref中的URL引用根据基本URI (https://example.com/schemas/customer)解析为 https://example.com/schemas/address.

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

假设在匿名模式中使用$ref,则无法解析相对引用。如下例中,/properties/shipping_address$ref可以正常解析,但 /properties/billing_address$ref则无法正常解析

{
  "type": "object",
  "properties": {
    "first_name": { "type": "string" },
    "last_name": { "type": "string" },
    "shipping_address": { "$ref": "https://example.com/schemas/address" },
    "billing_address": { "$ref": "/schemas/address" }
  },
  "required": ["first_name", "last_name", "shipping_address", "billing_address"]
}

$def

$defs关键字提供了一个标准化的位置来保存子模式,以便在当前模式文档中重用。

扩展前面的客户模式示例,为name 属性使用公共模式。为此定义一个新的模式是没有意义的,它只会在该模式中使用,因此可以选择使用$defs

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

$ref不仅仅有助于避免重复。它还可以用于编写更易于阅读和维护的模式。可以使用带有描述性名称的$defs来定义模式的复杂部分,并在需要的地方引用。

可以引用外部子模式,但通常将$ref限制为引用外部模式或$defs中定义的内部子模式。

递归

$ref关键字可以为指向的模式创建递归模式。例如,person模式中有一个children数组,而每个数组元素又是一个person实例:

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

上面创建了一个指向自身的模式,有效地在校验器中创建了一个“循环”。但需要注意,如下,在$ref引用另一个$ref可能会在解析器中导致无限循环。

{
  "$defs": {
    "alice": { "$ref": "#/$defs/bob" },
    "bob": { "$ref": "#/$defs/alice" }
  }
}

Bundling

在子模式中使用$id时,它表示一个嵌入的模式,它的标识符是$id的值,该值根据它出现在其中的模式的基本URI进行解析。包含嵌入模式的模式文档称为复合模式文档。复合模式文档中每个带有$id的模式称为模式资源。

本例显示了捆绑到复合模式文档中的客户模式示例和地址模式示例:

{
  "$id": "https://example.com/schemas/customer",
  "$schema": "https://json-schema.org/draft/2020-12/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 ..."] }
      }
    }
  }
}

无论模式资源是否捆绑,复合模式文档中的所有引用都必须相同。注意,客户模式中的$ref关键字没有变更。唯一的区别是,地址模式现在定义为/$defs/address,而不是单独的模式文档。你无法使用#/$defs/address引用地址模式,因为如果将模式拆分,该引用将不再指向地址模式。

此外还可以看到“$ref”:“#/definitions/state”解析为地址模式中的definitions关键字,而不是顶层模式中的definitions关键字,就像不使用嵌入模式时一样。

每个模式资源都是独立评估的,可以使用不同的JSON模式dialects。上面的示例中,地址模式资源使用Draft 7,而客户模式资源使用Draft 2020-12。如果嵌入式模式中没有声明$schema,则默认使用父模式的dialects。

posted @ 2022-02-26 15:18  charlieroro  阅读(702)  评论(0编辑  收藏  举报