理解 JSON Schema

本内容翻译自《Understanding JSON Schema》

JSON Schema 是用于验证 JSON 数据结构的强大工具,Schema可以理解为模式或者规则。然而,通过阅读它的规范来学习如何使用就像通过查看汽车的设计图来学习驾驶汽车。如果您只想买些杂货,那你是不需要知道电动机是如何组合在一起的。因此,本书旨在成为 JSON Schema 使用的友好讲师。它适用于那些想要编写并理解它,但可能目前对构建自己的汽车——呃,编写自己的 JSON 模式验证器不感兴趣的人。

笔记:本书主要是基于 JSON Schema Draft 7。早期版本的 JSON Schema 与这里描述的格式不完全兼容,但在大多数情况下,这些差异都会在文本中注明。

从哪里开始?

  • 本书使用了一些新颖的约定来展示模式示例并将 JSON 模式与您选择的编程语言相关联。
  • 如果您不确定什么是模式,请查看什么是模式?.
  • 基础章节应该足以让您开始了解核心JSON 模式参考
  • 当您开始开发具有许多嵌套和重复部分的大型模式时,请查看构建复杂模式
  • json-schema.org有很多资源,包括官方规范和使用各种编程语言的 JSON Schema 的工具。
  • 有许多在线 JSON 模式工具 允许您针对示例文档运行自己的 JSON 模式。如果您想在不安装任何软件的情况下进行尝试,这些会非常方便。

一、本书中使用的约定

特定语言注释

当来自另一种动态语言时,JavaScript 和 JSON 中基本类型的名称可能会令人困惑。我白天是一名 Python 程序员,所以当事物的名称与它们在 Python 中的名称不同时,我会在此处注明,以及任何其他特定于 Python 的使用 JSON 和 JSON Schema 的建议。我绝不试图对这本书产生 Python 偏见,但这是我所知道的,所以我从那里开始。从长远来看,我希望这本书对各行各业的程序员都有用,所以如果您有兴趣将 Python 参考翻译成 Algol-68 或您可能知道的任何其他语言,欢迎提出请求!

例如,这里有一个特定于语言的部分,其中包含有关在几种不同语言中使用 JSON 的建议:

在 Python 中,可以使用标准库中的 json 模块读取 JSON。

在 Ruby 中,可以使用 json gem 读取 JSON。对于 C,你可能要考虑使用Jansson来读写 JSON。

特定Draft注释

JSON Schema 标准已经过多次修订或“ Draft”。当前版本是 Draft 7,但 Draft 4 仍然被广泛使用。

编写该文本是为了鼓励使用Draft 7 并优先考虑最新的约定和功能,但在与早期版本不同的地方,这些差异在特殊标注中突出显示。如果您只想针对 Draft 7,您可以放心地忽略这些部分。

例子

本书中有许多示例,它们都遵循相同的格式。每个示例的开头是一个简短的 JSON 模式,说明了一个特定的原则,然后是针对该模式有效或无效的简短 JSON 片段。有效示例标记ok。无效的例子标记not ok。通常会有注释来解释为什么有效或无效。

笔记:每当构建本书时,这些示例都会自动进行测试,以保证它们不仅有用,而且正确!

例如,这是一个说明如何使用number 类型的片段:

{ "type": "number" }
42 // ok
-1 // ok
5.0 //简单的浮点数 ok
2.99792458e8 //指数计数法 ok
"42" // 作为字符串的数字 not ok

二、什么是 Schema ?

如果您曾经使用过 XML Schema、RelaxNG 或 ASN.1,您可能已经知道什么是模式,并且可以愉快地跳到下一部分。如果这一切对您来说听起来像天书,那么您来对地方了。要定义 JSON Schema 是什么,我们可能应该首先定义 JSON 是什么。

JSON 代表“JavaScript Object Notation”,一种简单的数据交换格式。它最初是作为万维网的符号。由于 JavaScript 存在于大多数 Web 浏览器中,并且 JSON 基于 JavaScript,因此很容易支持。然而,它已被证明足够有用且足够简单,以至于它现在被用于许多其他不涉及网上冲浪的环境中。

从本质上讲,JSON 建立在以下数据结构上:

  • 对象(object)

    { "key1": "value1", "key2": "value2" }

  • 数组(array)

    [ "first", "second", "third" ]

  • 数字(integer/number)

    42
    3.1415926
  • 字符串(string)

    "This is a string"

  • 布尔值(boolean)

    truefalse

  • null

    null

在大多数编程语言中都有类似类型,尽管它们可能有不同的名称。

下表从 JSON 类型的名称映射到它们在 Python 中的类似类型:

JSONPython
stringstring [4]
numberint/float [5]
objectdict
arraylist
booleanbool
nullNone

[^4]: 由于 JSON 字符串始终支持 unicode,因此它们类似于Python 2.x 中unicode和Python 3.x中的str

[^5]: JSON 没有单独的整数和浮点类型

下表将 JSON 类型的名称映射到它们在 Ruby 中的类似类型:

JSONRuby
stringString
numberInteger/Float [6]
objectHash
arrayArray
booleanTrueClass/FalseClass
nullNilClass

[^6]: 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 Schema 的用武之地。以下 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" }
}
}
}
}

通过针对此模式“验证”的一个失败案例如下:

{
"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 Schema 本身是用 JSON 编写的。它是数据本身,而不是计算机程序。它只是一种用于“描述其他数据结构”的声明性格式。这既是它的优点也是它的缺点(它与其他类似的模式语言共享)。简明地描述数据的表面结构并根据它自动验证数据很容易。但是,由于 JSON Schema 不能包含任意代码,因此在表达数据元素之间的关系上有所限制。因此,用于足够复杂的数据格式的任何“验证工具”都可能有两个验证阶段:一个在模式(或结构)级别,一个在语义级别。后一种检查可能需要使用更通用的编程语言来实现。

三、基础概览

《什么是 Schema?》里我们描述了什么是 Schema,并希望证明对 Schema 语言的需求是合理的。在这里,我们继续编写一个简单的 JSON Schema。

Hello, World!

在学习任何新语言时,从最简单的事情开始通常会很有帮助。在 JSON 模式中,空对象是一个完全有效的模式,它将接受任何有效的 JSON。

{} //这可以接受任何内容,只要它是有效的 JSON
42 // OK
"I'm a string" // OK
\{ "an": \[ "arbitrarily", "nested" \], "data": "structure" \} // OK

Draft 6 中的新内容

您还可以使用true代替空对象来表示匹配任何内容的模式,或者false表示不匹配任何内容的模式。

true // 该模式表示可以接受任何内容,只要它是有效的 JSON
42 // OK
"I'm a string" // OK
{ "an": [ "arbitrarily", "nested" ], "data": "structure" } // OK
false // 该模式表示不匹配任何内容
"Resistance is futile... This will always fail\!\!\!" //not OK

类型关键字

当然,如果我们只想接受任何 JSON 文档,我们就不会使用 JSON Schema。在 JSON Schema 中最常见的事情是限制为特定类型,type关键字就用于此。

当本书提到 JSON Schema“关键字”时,它指的是对象中键/值对的“键”部分。编写 JSON Schema 的大部分工作都涉及将特殊的“关键字”映射到对象中的值。

例如,在下面,只接受字符串:

{ "type": "string" }
"I'm a string" // OK
42 // not OK

type关键字在[特定类型关键字](#type-specific -keywords)进行了更详细的描述。

声明一个 JSON 模式

判断 JSON Schema 使用的是哪个draft并不总是那么容易。您可以使用$schema关键字来声明架构写入的 JSON 模式规范是哪个版本。更多信息,请参阅$schema。包含它通常是一种很好的做法,尽管它不是必需的。

笔记为简洁起见,本书的大多数示例中不包含$schema关键字,但在实际使用中应始终使用该关键字。

{ "$schema": "http://json-schema.org/draft-07/schema#" }

在 Draft 4 中,$schema值是 http://json-schema.org/schema#指最新版本的 JSON Schema。此用法已被弃用,并且需要使用特定版本的 URI。

声明唯一标识符

$id属性包含为每个模式的唯一标识符也是最佳实践。现在,只需将其设置为您控制的域中的 URL,例如:

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

当您开始构建复杂模式时,$id的细节将变得更加明显。

在draft 4 中,$id 只是 id(没有$符号)

四、JSON Schema 规范

数据类型

type关键字是 JSON Schema 的基础。它指定 Schema 的数据类型。

JSON Schema 的核心定义了以下基本类型:

在大多数编程语言中都有类似类型,尽管它们可能有不同的名称。

下表从 JSON 类型的名称映射到它们在 Python 中的类似类型:

JSONPython
stringstring [4]
numberint/float [5]
objectdict
arraylist
booleanbool
nullNone

[^4]: 由于 JSON 字符串始终支持 unicode,因此它们类似于Python 2.x 中unicode和Python 3.x中的str [^5]: JSON 没有单独的整数和浮点类型

下表将 JSON 类型的名称映射到它们在 Ruby 中的类似类型:

JSONRuby
stringString
numberInteger/Float [6]
objectHash
arrayArray
booleanTrueClass/FalseClass
nullNilClass

[^6]: JSON 没有单独的整数和浮点类型

type关键字可以是一个字符串或数组:

  • 如果是字符串,则是上述基本类型之一的名称。
  • 如果是数组,则必须是字符串数组,其中每个字符串是其中一种基本类型的名称,每个元素都是唯一的。在这种情况下,如果 JSON 片段与_任何_给定类型匹配,则它是有效的。

这是使用type关键字的简单示例:

{ "type": "number" }
42 // OK
42.0 // OK
"42" //not ok。这不是一个数字,它是一个包含数字的字符串。

在以下示例中,我们接受字符串和数字,但不接受结构化数据类型:

{ "type": ["number", "string"] }
42 // OK
"Life, the universe, and everything" // OK
["Life", "the universe", "and everything"] // not OK

对于这些类型中的每一种,都有仅适用于这些类型的关键字。例如,数字类型有一种指定数字范围的方法,这不适用于其他类型。在本参考中,这些验证关键字及其对应的每个类型都在后面章节中进行了描述。

字符串(string)

string类型用于文本字符串。它可能包含 Unicode 字符。

在 Python 中,“string”类似于Python 2.x 上的unicode和 Python 3.x 上的str类型。

在 Ruby 中,“string”类似于String 类型

{ "type": "string" }
"This is a string" // OK
"Déjà vu" // OK,Unicode 字符
"" // OK
"42" // OK
42 // not OK

长度

可以使用minLengthmaxLength关键字来限制字符串的长度。对于这两个关键字,该值必须是非负数。

{
"type": "string",
"minLength": 2,
"maxLength": 3
}
"A" // not ok
"AB" // ok
"ABC" // ok
"ABCD" // not ok

正则表达式

pattern关键字用于将字符串限制为特定的正则表达式。正则表达式语法是在 JavaScript(特别是ECMA 262 )中定义的。有关更多信息,请参阅正则表达式

笔记 在定义正则表达式时,重要的是要注意,如果表达式匹配字符串中的任何位置,则该字符串被认为是有效的。例如,正则表达式"p"将匹配任何包含一个p的字符串,例如"apple"不仅仅是一个简单的字符串"p"。因此,将正则表达式括在^...$中(例如,"^p$"),通常不会令人困惑,除非有充分的理由不这样做。

以下示例匹配一个带有可选区号的简单北美电话号码:

{
"type": "string",
"pattern": "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$"
}
"555-1212" // OK
"(888)555-1212" // OK
"(888)555-1212 ext. 532" // not OK
"(800)FLOWERS" // not OK

格式

format关键字允许对常用的某些类型的字符串值进行基本语义识别。例如,因为 JSON 没有“DateTime”类型,所以需要将日期编码为字符串。format允许模式作者指示字符串值应解释为日期。默认情况下,format 只是一个注释,不影响验证。

可选地,验证器实现可以提供一个配置选项来启用format作为断言而不仅仅是注释的功能。这意味着,如果具有date格式的值不是可以解析为日期的形式,则验证将失败。这可以允许值的约束超出 JSON Schema 中的其他工具,包括正则表达式可以执行的操作。

笔记 实例可能只为内置格式的一个子集提供验证,或者对给定格式进行部分验证。例如,一些实例可能会将包含@的字符串视为电子邮件,而其他实例可能会根据格式良好的电子邮件地址的对字符串进行额外检查。

JSON Schema 规范中偏向于与网络相关的格式,这很可能是由于其在 Web 技术方面的传统。但是,也可以使用自定义格式,只要交换 JSON 文档的各方也交换有关自定义格式类型的信息即可。JSON Schema 验证器将忽略它不理解的任何格式类型。

内置格式

以下是 JSON Schema 规范中指定的格式列表。

日期和时间

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

  • "date-time":日期和时间在一起,例如, 2018-11-13T20:20:39+00:00
  • "time":draft7的时间,例如,20:20:39+00:00
  • "date":draft7的日期,例如,2018-11-13
电子邮件地址
  • "email":Internet 电子邮件地址,请参阅RFC 5322,第 3.4.1 节
  • "idn-email":draft7的新内容Internet 电子邮件地址的国际化形式,请参阅 RFC 6531
主机名
IP 地址
资源标识符
  • "uri":根据RFC3986 的通用资源标识符 (URI) 。
  • "uri-reference":draft7 6 中的新增内容,一个 URI 引用(URI 或相对引用),根据RFC3986 第 4.1 节
  • "iri":draft 7 中的新内容,根据RFC3987,“uri”的国际化等价物。
  • "iri-reference":draft7中的新内容,根据RFC3987,“uri-reference”的国际化等价物

如果模式中的值能够与特定的源路径(例如来自网页的链接)相关联,那么使用"uri-reference"(or "iri-reference") 而不是"uri"(or "iri")通常是更好的做法 。"uri"只应在路径必须是绝对路径时使用。

  • draft 4 只包括"uri",不包括"uri-reference"。因此,是否"uri"应该接受相对路径存在一些歧义。
URI 模板
  • "uri-template":draft 6 中的新增内容,一个 URI 模板(任何级别)根据 RFC6570。如果您还不知道 URI 模板是什么,您可能不需要这个值。
JSON 指针
  • "json-pointer":draft6 中的新内容,一个 JSON 指针,根据RFC6901。在构建复杂模式中有更多关于在 JSON Schema 中使用 JSON Pointer 的讨论。请注意,仅当整个字符串仅包含 JSON 指针内容时才应使用此方法,例如 /foo/bar. JSON 指针 URI 片段,例如#/foo/bar/应该使用 "uri-reference".
  • "relative-json-pointer":draft7 中的新内容,一个相对 JSON 指针
正则表达式
  • "regex":draft7中的新内容,正则表达式,根据ECMA 262 应有效。

请注意,在实践中,JSON 模式验证器只需要接受本文档其他地方描述的正则表达式的安全子集。

正则表达式

模式模式属性关键字使用正则表达式来表示约束。使用的正则表达式语法来自 JavaScript(特别是ECMA 262)。但是,该完整语法并未得到广泛支持,因此建议您坚持使用下面描述的该语法的子集。

  • 单个 unicode 字符(下面的特殊字符除外)与其自身匹配。

  • .: 匹配除换行符以外的任何字符。(请注意,换行符的构成在某种程度上取决于您的平台和语言环境,但实际上这很少重要)。

  • ^: 只匹配字符串的开头。

  • $: 仅在字符串末尾匹配。

  • (...): 将一系列正则表达式组合成一个正则表达式。

  • |: 匹配|符号之前或之后的正则表达式。

  • [abc]: 匹配方括号内的任何字符。

  • [a-z]: 匹配字符范围。

  • [^abc]: 匹配任何_未_列出的字符。

  • [^a-z]: 匹配范围外的任何字符。

  • +: 匹配前面正则表达式的一个或多个重复项。

  • *: 匹配前面正则表达式的零次或多次重复。

  • ?: 匹配前面正则表达式的零次或一次重复。

  • +?, *?, ??: *, +, 和?限定符都是贪婪的;它们匹配尽可能多的文本。有时不需要这种行为,您希望匹配尽可能少的字符。

  • (?!x), (?=x):积极或消极地向前查找。

    exp1(?!exp2):查找后面不是 exp2 的 exp1

    exp1(?=exp2):查找后面是 exp2 的 exp1

  • {x}: 完全x匹配前面的正则表达式。

  • {x,y}: 匹配至少x和最多y出现的前面的正则表达式。

  • {x,}: 匹配x前面的正则表达式中出现的一个或多个。

  • {x}?, {x,y}?, {x,}?: 上述表达式的懒惰版本。

示例

以下示例匹配一个带有可选区号的简单北美电话号码:

{
"type": "string",
"pattern": "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$"
}
"555-1212" // OK
"(888)555-1212" // OK
"(888)555-1212 ext. 532" // not OK
"(800)FLOWERS" // not OK

数字类型(integer/number)

JSON Schema 中有两种数字类型:integernumber。它们共享相同的验证关键字。

笔记:JSON 没有表示复数的标准方法,因此无法在 JSON Schema 中测试它们。

integer

integer类型用于整数。JSON 没有针对整数和浮点值的不同类型。因此,有无小数点并不足以区分整数和非整数。例如,11.0是在 JSON 中表示相同值的两种方式。无论使用哪种表示形式,JSON 模式都将该值视为整数。

在 Python 中,“integer”类似于int类型

在 Ruby 中,“integer”类似于Integer类型

{ "type": "integer" }
42 // OK
\-1 // OK
1.0 // OK,小数部分为零的数字被视为整数
3.1415926 // not OK,浮点数被拒绝
"42" // not OK,作为字符串的数字被拒绝

number

number类型用于任何数字类型,整数或浮点数。

在 Python 中,“数字”类似于float类型。在 Ruby 中,“数字”类似于Float类型。

{ "type": "number" }
42 // OK
\-1 // OK
5.0 // OK,简单的浮点数
2.99792458e8 // OK,指数符号也有效
"42" // not OK,作为字符串的数字被拒绝

倍数

可以使用multipleOf关键字将数字限制为给定数字的倍数 。它可以设置为任何正数。

{
"type": "number",
"multipleOf" : 10
}
0 // OK
10 // OK
20 // OK
23 // not OK,不是 10 的倍数

范围

数字的范围是使用minimummaximum关键字的组合指定的 (或exclusiveMinimumexclusiveMaximum用于表示排他范围)。

如果x是要验证的值,则以下必须成立:

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

虽然您可以同时指定minimumexclusiveMinimum或同时 指定maximumexclusiveMaximum,但这样做没有意义。

{
"type": "number",
"minimum": 0,
"exclusiveMaximum": 100
}
-1 // not OK,小于0
0 // OK
10 // OK
99 // OK
100 // not OK
101 // not OK
Draft 4

在 JSON Schema draft4中,exclusiveMinimumexclusiveMaximum工作方式不同。它们是布尔值,指示是否 minimummaximum不包括该值。例如:

  • 如果exclusiveMinimumfalsexminimum
  • 如果exclusiveMinimumtrue, x > minimum

新版本已更改为具有更好的关键字独立性。这是一个使用旧Draft 4 约定的示例:

{
"type": "number",
"minimum": 0,
"maximum": 100,
"exclusiveMaximum": true
}
-1 // not OK,小于0
0 // OK
10 // OK
99 // OK
100 // not OK
101 // not OK

对象(object)

对象是 JSON 中的映射类型。他们将“键”映射到“值”。在 JSON 中,“键”必须始终是字符串。这些对中的每一组通常被称为“属性”。

在 Python 中,“对象”类似于dict类型。然而,一个重要的区别是,虽然 Python 字典可以使用任何可散列的键作为键,但在 JSON 中,所有键都必须是字符串。尽量不要被此处“对象”一词的两种用法所混淆:Python 使用该词object来表示所有事物的通用基类,而在 JSON 中,它仅用于表示从字符串键到值的映射。

在 Ruby 中,“对象”类似于Hash类型。然而,一个重要的区别是 JSON 中的所有键都必须是字符串,因此任何非字符串键都被转换为它们的字符串表示。尽量不要被这里“对象”一词的两种用法所混淆:Ruby 使用这个词Object来表示所有事物的通用基类,而在 JSON 中,它仅用于表示从字符串键到值的映射。

{ "type": "object" }
{// OK
"key": "value",
"another_key": "another_value"
}
{// OK
"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
}
{// not OK,使用非字符串作为键是无效的 JSON
0.01: "cm",
1: "m",
1000: "km"
}
"Not an object" // not OK,使用非字符串作为键是无效的 JSON
["An", "array", "not", "an", "object"] // not OK

属性

对象的属性(键值对)是使用properties关键字定义的 。properties的值是一个对象,其中每个键是属性的名称,每个值是用于验证该属性的模式。此properties关键字将忽略与关键字中的任何属性名称不匹配的任何属性。

注意:禁止不符合任何属性名称的属性properties,请参阅附加属性

例如,我们要为由数字、街道名称和街道类型组成的地址定义一个简单的模式:

{
"type": "object",
"properties": {
"number": { "type": "number" },
"street_name": { "type": "string" },
"street_type": { "enum": ["Street", "Avenue", "Boulevard"] }
}
}
// OK
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" }
// not OK,提供的号码类型错误,则无效
{ "number": "1600", "street_name": "Pennsylvania", "street_type": "Avenue" }
// OK,默认情况下,省略属性是有效的。请参阅必需属性。
{ "number": 1600, "street_name": "Pennsylvania" }
// OK,通过扩展,即使是空对象也是有效的
{ }
// OK,默认情况下,提供附加属性是有效的:
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "direction": "NW" }

模式属性

有时您想说,给定一种特定类型的属性名称,该值应该与特定模式相匹配。这就是patternProperties起作用的地方 :它将正则表达式映射到模式。如果属性名称与给定的正则表达式匹配,则属性值必须针对相应的架构进行验证。

注意:正则表达式是没有锚定的,这意味着在为模式属性定义正则表达式时,需要注意该表达式可能与属性名称内的任何位置匹配。例如,正则表达式"p"将匹配任何包含一个p的属性名称(例如"apple"),而不仅仅是名称为"p"。因此,将正则表达式括在^...$ 中通常比较容易理解,例如,"^p$"

在以下示例中,名称以前缀开头的任何属性都S_必须是字符串,并且任何具有前缀的属性都 I_必须是整数。任何与任一正则表达式都不匹配的属性将被忽略。

{
"type": "object",
"patternProperties": {
"^S_": { "type": "string" },
"^I_": { "type": "integer" }
}
}
// OK
{ "S_25": "This is a string" }
// OK
{ "I_0": 42 }
// not OK,如果名称以 开头S_,则必须是字符串
{ "S_0": 42 }
// not OK,如果名称以 开头I_,则必须是整数
{ "I_42": "This is a string" }
// OK这是一个不匹配任何正则表达式的键
{ "keyword": "value" }

额外属性

additionalProperties关键字用于控制的额外的东西,那就是性能,其名称没有在 properties 关键字中列出的或与 patternProperties 关键字中的任何正则表达式匹配的属性。默认情况下,允许任何其他属性。

additionalProperties关键字的值是一个模式,将用于验证实例中与properties或不匹配的任何属性patternProperties。将additionalProperties架构设置 为false意味着不允许其他属性。

重用Properties的示例,但这次设置 additionalPropertiesfalse.

{
"type": "object",
"properties": {
"number": { "type": "number" },
"street_name": { "type": "string" },
"street_type": { "enum": ["Street", "Avenue", "Boulevard"] }
},
"additionalProperties": false
}
// OK
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" }
// not OK,额外属性“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" }
}
// OK
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" }
// OK,这是有效的,因为附加属性的值是一个字符串
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "direction": "NW" }
// not OK,这是无效的,因为附加属性的值不是字符串:
{ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "office_number": 201 }

您可以additionalPropertiespropertiespatternProperties组合起来使用。在以下示例中,基于Pattern Properties 中的示例,我们添加了一个"builtin" 属性,该属性必须是数字,并声明所有其他属性(既不符合 properties定义,同时不匹配 patternProperties)必须是字符串:

{
"type": "object",
"properties": {
"builtin": { "type": "number" }
},
"patternProperties": {
"^S_": { "type": "string" },
"^I_": { "type": "integer" }
},
"additionalProperties": { "type": "string" }
}
// OK
{ "builtin": 42 }
// OK,这是一个不匹配任何正则表达式的键:
{ "keyword": "value" }
// not OK,额外属性必须是一个字符串:
{ "keyword": 42 }

必须属性

默认情况下,properties不需要关键字定义的属性。但是,可以使用required关键字提供所需属性的列表。

required关键字采用零个或多个字符串的数组。这些字符串中的每一个都必须是唯一的。

  • 在dreft 4 中,required必须至少包含一个字符串。
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"address": { "type": "string" },
"telephone": { "type": "string" }
},
"required": ["name", "email"]
}
// OK
{
"name": "William Shakespeare",
"email": "bill@stratford-upon-avon.co.uk"
}
// OK,提供额外的属性是可以的,即使是架构中没有定义的属性:
{
"name": "William Shakespeare",
"email": "bill@stratford-upon-avon.co.uk",
"address": "Henley Street, Stratford-upon-Avon, Warwickshire, England",
"authorship": "in question"
}
// not OK,缺少必需的“email”属性会使 JSON 文档无效
{
"name": "William Shakespeare",
"address": "Henley Street, Stratford-upon-Avon, Warwickshire, England",
}
// not OK,在 JSON 中,具有值的属性null不等同于不存在的属性。这失败,因为null不是“字符串”类型,而是“空”类型
{
"name": "William Shakespeare",
"address": "Henley Street, Stratford-upon-Avon, Warwickshire, England",
"email": null
}

属性名称

draft6 中的新内容

可以根据模式验证属性名称,而不管它们的值。如果您不想强制执行特定属性,但您想确保这些属性的名称遵循特定约定,这会很有用。例如,您可能想要强制所有名称都是有效的 ASCII 标记,以便它们可以用作特定编程语言中的属性。

{
"type": "object",
"propertyNames": {
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
}
}
// OK
{
"_a_proper_token_001": "value"
}
// not OK
{
"001 invalid": "value"
}

由于对象键无论如何必须始终是字符串,因此暗示给定的模式propertyNames始终至少为:

{ "type": "string" }

属性数量

可以使用minPropertiesmaxProperties关键字来限制对象上的属性数量 。这些中的每一个都必须是非负整数。

{
"type": "object",
"minProperties": 2,
"maxProperties": 3
}
{} // not OK
{ "a": 0 } // not OK
{ "a": 0, "b": 1 } // OK
{ "a": 0, "b": 1, "c": 2 } // OK
{ "a": 0, "b": 1, "c": 2, "d": 3 } // not OK

数组(array)

数组用于有序元素。在 JSON 中,数组中的每个元素可能是不同的类型。

在 Python 中,“数组”类似于 listtuple类型,具体取决于用法。但是,jsonPython 标准库中的模块将始终使用 Python 列表来表示 JSON 数组。

在 Ruby 中,“数组”类似于Array类型。

{ "type": "array" }
[1, 2, 3, 4, 5] // OK
[3, "different", { "types" : "of values" }] // OK
{"Not": "an array"} // not OK

元素

JSON 中数组的使用一般有两种方式:

  • **列表验证:**任意长度的序列,其中每个项目都匹配相同的模式。
  • **元组验证:**一个固定长度的序列,其中每个项目可能有不同的模式。在这种用法中,每个项目的索引(或位置)对于如何解释值是有意义的。(在某些编程语言中,这种用法通常被赋予一个完整的单独类型,例如 Python 的tuple)。
列表验证

列表验证对于任意长度的数组很有用,其中每个项目都匹配相同的模式。对于这种类型的数组,将items关键字设置为单个模式,将用于验证数组中所有元素。

笔记:当items是单模式时,additionalItems关键字没有意义,不应使用。

在下面的例子中,我们定义数组中的每一项都是一个数字:

{
"type": "array",
"items": {
"type": "number"
}
}
[1, 2, 3, 4, 5] // OK
[1, 2, "3", 4, 5] // not OK,单个“非数字”会导致整个数组无效
[] // OK,空数组始终有效
元组验证

当数组是一个元素的集合时,元组验证很有用,其中每个项目都有不同的架构并且每个项目的序数索引是有意义的。

例如,您可以表示街道地址,例如:

1600 Pennsylvania Avenue NW

作为以下形式的 4 元组:

[号码、街道名称、街道类型、方向]

这些字段中的每一个都将具有不同的模式:

  • number: 地址编号,必须是数字。
  • street_name: 街名,必须是字符串。
  • street_type: 街道类型,应该是来自一组固定值的字符串。
  • direction:地址所在城市象限,应该是来自不同值组成集合的字符串。

为此,我们将items关键字设置为一个数组,其中每个项目都是一个模式,对应于文档数组的每个索引。也就是说,一个数组,其中第一个元素验证输入数组的第一个元素,第二个元素验证输入数组的第二个元素,依此类推。

以下是示例:

{
"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”不是可接受的街道类型之一
["Palais de l'Élysée"] // not OK,此地址缺少街道号码
[10, "Downing", "Street"] // OK,可以不提供所有项目
[1600, "Pennsylvania", "Avenue", "NW", "Washington"] // OK,默认情况下可以在尾部添加其他项目

附加元素

使用additionalItems关键字控制如果有超过元组内items属性定义的附加元素,元组是否有效。additionalItems关键字的值是一个模式,所有其他项目必须通过该模式才能验证关键字。如果items 同一模式中不存在“元组验证”关键字,则忽略此关键字。

在Draft 4 中,additionalItems不需要存在“元组验证”items关键字。对任何项目都没有限制,因此所有项目都被视为附加项目。

在这里,我们将重用上面的示例模式,但设置 additionalItemsfalse,这具有禁止数组中的额外项目的效果。

{
"type": "array",
"items": [
{ "type": "number" },
{ "type": "string" },
{ "enum": ["Street", "Avenue", "Boulevard"] },
{ "enum": ["NW", "NE", "SW", "SE"] }
],
"additionalItems": false
}
[1600, "Pennsylvania", "Avenue", "NW"] // OK
[1600, "Pennsylvania", "Avenue"] // OK,可以不提供所有元素
[1600, "Pennsylvania", "Avenue", "NW", "Washington"] // not OK,不能提供额外的元素

您可以通过使用非布尔模式来限制附加项可以具有的值来表达更复杂的约束。在这种情况下,我们可以说允许附加元素,只要它们都是字符串:

{
"type": "array",
"items": [
{ "type": "number" },
{ "type": "string" },
{ "enum": ["Street", "Avenue", "Boulevard"] },
{ "enum": ["NW", "NE", "SW", "SE"] }
],
"additionalItems": { "type": "string" }
}
[1600, "Pennsylvania", "Avenue", "NW", "Washington"] // OK,额外的字符串元素是可以的
[1600, "Pennsylvania", "Avenue", "NW", 20500] // not OK,额外的元素不是字符串

注意:因为“列表验证”(items是一个对象)适用于列表中的所有项目,所以这三个项目没有附加项目,因此 additionalItems没有任何可应用其模式的内容,也不会产生任何影响。

包含

Draft 6 中的新内容:虽然items模式必须对数组中的每一项都有效,但 contains模式只需要针对数组中的一项或多项进行验证。

{
"type": "array",
"contains": {
"type": "number"
}
}
["life", "universe", "everything", 42] // OK,包含一个number元素
["life", "universe", "everything", "forty-two"] // not OK,不包含number元素
[1, 2, 3, 4, 5] // OK

长度

可以使用minItemsmaxItems关键字指定数组的长度。每个关键字的值必须是非负数。无论是进行List 验证还是Tuple 验证,这些关键字都 有效

{
"type": "array",
"minItems": 2,
"maxItems": 3
}
[] // not OK,
[1] // not OK,
[1, 2] // OK
[1, 2, 3] // OK
[1, 2, 3, 4] // not OK

唯一性

只需将uniqueItems关键字设置为true,可以限制数组中的每个元素都是唯一的。

{
"type": "array",
"uniqueItems": true
}
[1, 2, 3, 4, 5] // OK
[1, 2, 3, 3, 4] // not OK
[] // OK空数组总是通过

布尔值(boolean)

布尔类型只匹配两个特殊值:truefalse。请注意,模式不接受其他约定为truefalse的值,例如 1 和 0。

在 Python 中,“boolean”类似于bool类型。请注意,在 JSON 中, trueandfalse是小写的,而在 Python 中,它们是大写的 ( Trueand False)。

在 Ruby 中,“boolean”类似于TrueClassFalseClass。请注意,在 Ruby 中没有Boolean类。

{ "type": "boolean" }
true // OK
false // OK
"true" // not OK
0 // not OK

NULL(null)

当一个模式指定 typenull时,它只有一个可接受的值:null

注意:在 JSON 中,null不等于缺少某些东西。有关示例,请参阅必需属性

在 Python 中,null类似于None.

在 Ruby中, null 类似于 nil.

{ "type": "null" }
null // OK
false // not OK
0 // not OK
"" // not OK

通用关键字

本章列出了一些适用于所有 JSON 类型的杂项属性。

注释

JSON Schema 包含一些关键字,它们并不严格用于验证,而是用于描述模式的一部分。这些“注释”关键字都不是必需的,但鼓励使用为了良好实践,并且可以使您的模式“自我记录”。

titledescription关键字必须是字符串。title最好是简短的,而description提供模式描述的数据因此会有更长的说明。

default关键字指定一个默认值。该值不用于在验证过程中填充缺失值。文档生成器或表单生成器等非验证工具可能会使用此值提示用户如何使用该值。但是,default通常用于表示如果缺少某个值,则该值在语义上与该值与默认值一起存在时的语义相同。default的值应该根据它所在的模式进行验证,但这不是必需的。

Draft 6 中的新内容 examples关键字是提供一系列针对模式进行验证的示例的地方。这不用于验证,但可能有助于向读者解释模式的效果和目的。每个条目都应该根据它所在的模式进行验证,但这并不是严格要求的。没有必要复制examples数组中的default值,因为 default将被视为另一个示例。

Draft 7 中的新内容 布尔类型的关键字readOnlywriteOnly通常用于 API 上下文中。readOnly表示该值可读不可改,可用于说明一个更改值的PUT请求将得到一个400 Bad Request的响应。writeOnly表示该值可已修改但是不可以读,可用于说明可通过PUT请求来设置值,但通过GET请求来检索该记录时不能获取该值 。

Draft2019-09的新内容 deprecated关键字是一个布尔值,表明该关键字应用的实例值不宜使用,并可能在将来被移除。

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

评论

Draft 7 中的新内容 $comment关键字严格用于向模式添加注释。它的值必须始终是一个字符串。与注解 titledescriptionexamples不同, JSON 模式实现不允许附加任何含义或行为,甚至可以随时剥离它们。因此,它们对于给 JSON 模式的未来编辑者留下笔记很有用,但不宜用于与模式的用户进行交流。

枚举值

enum关键字用于将值限制为一组固定的值。它必须是一个包含至少一个元素的数组,其中每个元素都是唯一的。

以下是验证路灯颜色的示例:

{
"enum": ["red", "amber", "green"]
}
"red" // OK
"blue" // not OK

您甚至可以使用enum添加没有类型的值,让我们扩展示例,用null指示“off”,并添加 42,只是为了好玩。

{
"enum": ["red", "amber", "green", null, 42]
}
"red" // OK
null // OK
42 // OK
0 // not OK

常量值

Draft 6 中的新内容 const关键字被用于限制值为一个常量值。

例如,如果出于出口原因仅支持运送到美国:

{
"properties": {
"country": {
"const": "United States of America"
}
}
}
{ "country": "United States of America" } // OK
{ "country": "Canada" } // not OK

Media:字符串编码非 JSON 数据

Draft 7 中的新内容

JSON 模式有一组关键字来描述和可选地验证存储在 JSON 字符串中的非 JSON 数据。由于很难为许多媒体类型编写验证器,因此不需要 JSON 模式验证器根据这些关键字验证 JSON 字符串的内容。但是,这些关键字对于使用经过验证的 JSON 的应用程序仍然有用。

内容媒体类型

contentMediaType关键字指定的MIME类型的字符串的内容,如在RFC 2046。有一个由 IANA 正式注册MIME 类型列表,但支持的类型集将取决于应用程序和操作系统。Mozilla Developer Network 还维护了一个较短的对网络很重要的 MIME 类型列表

内容编码

contentEncoding关键字指定编码用于存储内容,如在规定的RFC 2054,部分6.1RFC 4648

可接受的值为7bit8bitbinaryquoted-printablebase16base32,和base64。如果未指定,则编码与包含的 JSON 文档相同。

在不深入了解每种编码的底层细节的情况下,实际上只有两个选项对现代使用有用:

  • 如果内容使用与封闭 JSON 文档相同的编码(出于实际目的,几乎总是 UTF-8),请保持 contentEncoding未指定,并将内容按原样包含在字符串中。这包括基于文本的内容类型,例如text/htmlapplication/xml
  • 如果内容是二进制数据,则设置contentEncodingbase64并使用Base64对内容进行编码。这将包括许多图像类型,例如image/png或音频类型,例如audio/mpeg.

内容模式

2019-09 Draft中的新内容 文档即将推出

例子

以下模式指示字符串包含一个 HTML 文档,使用与周围文档相同的编码进行编码:

{
"type": "string",
"contentMediaType": "text/html"
}
// OK
"<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head></head></html>"

以下模式指示字符串包含使用 Base64 编码的PNG图像:

{
"type": "string",
"contentEncoding": "base64",
"contentMediaType": "image/png"
}
// OK
"iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAA..."

Schema 组合

JSON Schema 包含一些用于将模式组合在一起的关键字。请注意,这并不一定意味着组合来自多个文件或 JSON 树的模式,尽管这些工具有助于实现这一点,并且在构建复杂模式中进行了描述。组合模式可能就像允许同时根据多个标准验证一个值一样简单。

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

用于组合模式的关键字是:

  • allOf : (AND) 必须对_所有_子模式有效
  • anyOf : (OR) 必须对_任何子_模式有效
  • oneOf : (XOR) 必须对_恰好一个_子模式有效

所有这些关键字都必须设置为一个数组,其中每个项目都是一个模式。

此外,还有:

  • not : (NOT)_不能_对给定的模式有效

allOf

要验证allOf,给定的数据必须针对给定的所有子模式有效。

{
"allOf": [
{ "type": "string" },
{ "maxLength": 5 }
]
}
"short" // OK
"too long" // not OK

注意:在面向对象继承的意义上, allOf不能用于“扩展”模式以向其添加更多细节。实例必须对 allOf包含每一个模式都有效. 有关更多信息,请参阅有关子模式独立性的部分。

anyOf

要验证anyOf,数据必须满足任意一个或多个给定子模式。

{
"anyOf": [
{ "type": "string", "maxLength": 5 },
{ "type": "number", "minimum": 0 }
]
}
"short" // OK
"too long" // not OK
12 // OK
-5 // not OK

oneOf

要验证oneOf,数据必须满足且只满足一个给定的子模式。

{
"oneOf": [
{ "type": "number", "multipleOf": 5 },
{ "type": "number", "multipleOf": 3 }
]
}
10 // OK
9 // OK
2 // not OK,不是 5 或 3 的倍数。
15 // not OK,同时符合两个子模式被拒绝。

not

要验证not,数据不能满足给定的子模式。

例如,以下模式针对不是字符串的任何内容进行验证:

{ "not": { "type": "string"} }
42 // OK
{ "key": "value" } // OK
"I am a string" // not OK

模式组合的属性

子模式独立

注意:allOfanyOfoneOf数组中列出的模式彼此一无所知。例如,假设您在一个$defs部分中有一个地址的架构,并且想要“扩展”它以包含地址类型:

{
"$defs": {
"address": {
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" }
},
"required": ["street_address", "city", "state"]
}
},
"allOf": [
{ "$ref": "#/$defs/address" },
{
"properties": {
"type": { "enum": [ "residential", "business" ] }
}
}
]
}
{// OK
"street_address": "1600 Pennsylvania Avenue NW",
"city": "Washington",
"state": "DC",
"type": "business"
}

这是可行的,但是如果我们想限制模式以便不允许附加属性怎么办?可以尝试添加"additionalProperties": false

{
"$defs": {
"address": {
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" }
},
"required": ["street_address", "city", "state"]
}
},
"allOf": [
{ "$ref": "#/$defs/address" },
{
"properties": {
"type": { "enum": [ "residential", "business" ] }
}
}
],
"additionalProperties": false
}
// not OK
{
"street_address": "1600 Pennsylvania Avenue NW",
"city": "Washington",
"state": "DC",
"type": "business"
}

不幸的是,现在模式将拒绝_一切_。这是因为additionalPropertiesallOf数组内的子模式中声明的属性一无所知

对许多人来说,这是 JSON 模式中组合操作的最大惊喜之一:它的行为不像面向对象语言中的继承。在 JSON 模式规范的下一版本中,有一些建议可以解决这个问题。

不合逻辑的模式

请注意,使用这些关键字创建逻辑上不可能的模式非常容易。以下示例创建了一个不会针对任何内容进行验证的架构(因为某些内容可能不会同时是字符串和数字):

{
"allOf": [
{ "type": "string" },
{ "type": "number" }
]
}
"No way" // not OK
-1 // not OK
分解模式

请注意,可以“分解”子模式的公共部分。以下两个模式是等效的。

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

有条件地应用子模式

必要依赖

dependentRequired关键字有条件地要求,如果一个对象存在某个特定的属性,则另一个属性也必须存在。例如,假设我们有一个表示客户的模式,如果您有他们的信用卡号,您还需要确保您有账单地址。如果您没有他们的信用卡号,则不需要帐单邮寄地址。我们使用dependentRequired关键字表示一个属性对另一个属性的这种依赖性。dependentRequired关键字的值是一个对象。对象中的每个条目都从属性的名称_p_映射到一个字符串数组,其中列出了_p_存在时所需的属性。

在下面的例子中,无论何时,只要存在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
}
// OK。这没关系,因为我们既没有credit_carda也没有billing_address。
{
"name": "John Doe"
}
// OK。请注意,依赖项不是双向的。有一个没有信用卡号的帐单地址是可以的。
{
"name": "John Doe",
"billing_address": "555 Debtor's Lane"
}

要解决上面的最后一个问题(依赖项不是双向的),您当然可以明确定义双向依赖项:

{
"type": "object",
"properties": {
"name": { "type": "string" },
"credit_card": { "type": "number" },
"billing_address": { "type": "string" }
},
"required": ["name"],
"dependentRequired": {
"credit_card": ["billing_address"],
"billing_address": ["credit_card"]
}
}
// not OK,这个实例有一个credit_card,但缺少一个billing_address。
{
"name": "John Doe",
"credit_card": 5555555555555555
}
// not OK,这有一个billing_address,但缺少一个credit_card。
{
"name": "John Doe",
"billing_address": "555 Debtor's Lane"
}

Draft 4-7Draft2019-09之前的版本,dependentRequireddependentSchemas被称为一个关键字dependencies。如果依赖值是一个数组,它的行为就像一个 dependentRequired,如果依赖值是一个模式,它的行为就像dependentSchema.

模式依赖

dependenciesSchemas关键字要求当给定的属性存在时,有条件地应用子模式。此架构的应用方式与allOf应用架构的方式相同。没有合并或扩展任何内容。两种模式独立应用。

例如,这里有另一种写法:

{
"type": "object",
"properties": {
"name": { "type": "string" },
"credit_card": { "type": "number" }
},
"required": ["name"],
"dependentSchemas": {
"credit_card": {
"properties": {
"billing_address": { "type": "string" }
},
"required": ["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
}
// OK。这有一个billing_address,但缺少一个 credit_card。这通过了,因为这里billing_address 看起来像一个附加属性:
{
"name": "John Doe",
"billing_address": "555 Debtor's Lane"
}

Draft 4-7 Draft2019-09之前的版本,dependentRequireddependentSchemas被称为一个关键字dependencies。如果依赖值是一个数组,它的行为就像一个 dependentRequired,如果依赖值是一个模式,它的行为就像dependentSchema.

[条件语句]

新的Draft7中 ifthenelse关键字允许基于另一种模式的结果来应用子模式,这很像传统编程语言中的if/ then/else构造。

如果if有效,then也必须有效(并被else忽略)。如果 if无效,else也必须有效(并被then忽略)。

如果thenelse未定义,则if表现为它们的值为true

如果then和/或else出现在没有if,then和 的模式中,else则被忽略。

我们可以把它放在真值表的形式中,显示 when if, then, and elseare valid的组合 以及整个模式的结果有效性:

ifthenelsewhole schema
TTn/aT
TFn/aF
Fn/aTT
Fn/aFF
n/an/an/aT

例如,假设您想编写一个模式来处理美国和加拿大的地址。这些国家/地区有不同的邮政编码格式,我们希望根据国家/地区选择要验证的格式。如果地址在美国,则该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"
}
// OK
{
"street_address": "1600 Pennsylvania Avenue NW",
"postal_code": "20500"
}
// OK
{
"street_address": "24 Sussex Drive",
"country": "Canada",
"postal_code": "K1M 1M4"
}
// not OK
{
"street_address": "24 Sussex Drive",
"country": "Canada",
"postal_code": "10000"
}
// not OK
{
"street_address": "1600 Pennsylvania Avenue NW",
"postal_code": "K1M 1M4"
}

笔记 :在此示例中,“国家/地区”不是必需的属性。因为“if”模式也不需要“country”属性,它会pass然后应用“then”模式。因此,如果未定义“country”属性,则默认行为是将“postal_code”验证为美国邮政编码。“default”关键字没有效果,但将其包含在模式中,对读者比较友好,可以更容易地识别默认行为。

不幸的是,上面的这种方法不能扩展到两个以上的国家。但是,您可以将ifthen包裹在allOf中以创建可扩展的内容。在此示例中,我们将使用美国和加拿大邮政编码,但还会添加荷兰邮政编码,即 4 位数字后跟两个字母。读者可以尝试练习将其扩展到世界上其余的邮政编码。

{
"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}" } }
}
}
]
}
// OK
{
"street_address": "1600 Pennsylvania Avenue NW",
"country": "United States of America",
"postal_code": "20500"
}
// OK
{
"street_address": "1600 Pennsylvania Avenue NW",
"postal_code": "20500"
}
// OK
{
"street_address": "24 Sussex Drive",
"country": "Canada",
"postal_code": "K1M 1M4"
}
// OK
{
"street_address": "Adriaan Goekooplaan",
"country": "Netherlands",
"postal_code": "2517 JX"
}
// not OK
{
"street_address": "24 Sussex Drive",
"country": "Canada",
"postal_code": "10000"
}
// not OK
{
"street_address": "1600 Pennsylvania Avenue NW",
"postal_code": "K1M 1M4"
}

笔记 “if”模式中的“required”关键字是必需的,否则如果未定义“country”,则它们都将适用。如果未定义“country”,则将“required”从“United States of America”“IF”模式中删除,使其有效地成为默认值。

笔记 即使“country”是必填字段,仍然建议在每个“if”模式中使用“required”关键字。验证结果将相同,因为“required”将失败,但不包括它会增加错误结果的噪音,因为它将针对所有三个“then”模式验证“postal_code”,导致不相关的错误。

蕴含

在 Draft 7 之前,您可以使用模式组合关键字和称为“蕴含”的布尔代数概念来表达“if-then”条件 。A -> B(A 隐含 B)意味着如果 A 为真,那么 B 也必须为真。它表示为 JSON Schema可以这样写 !A || B

{
"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"] }
]
}
// OK
{
"restaurantType": "sit-down",
"total": 16.99,
"tip": 3.4
}
// not OK
{
"restaurantType": "sit-down",
"total": 16.99
}
// OK
{
"restaurantType": "fast-food",
"total": 6.99
}
// OK
{ "total": 5.25 }

蕴涵的变化可以用来表达你用if/ then/else关键字表达的内容。 if/then可表示为A -> Bif/ else可表示为!A -> Bif/ then/else可表示为A -> B AND !A -> C

笔记由于此模式不是很直观,因此建议将您在$defs中的 条件语句与描述性名称一起, 结合"allOf": [{ "$ref": "#/$defs/sit-down-restaurant-implies-tip-is-required" }]一起$ref入您的模式中。

声明方言

JSON Schema 的一个版本称为方言。方言表示可用于评估模式的一组关键字和语义。每个 JSON Schema 版本都是 JSON Schema 的新方言。JSON Schema 为您提供了一种声明模式符合哪种方言的方法,并提供了描述您自己的自定义方言的方法。

$schema

$schema关键字用于声明模式是针对哪种 JSON 方言编写的。$schema 关键字的值也是模式的标识符,可用于根据方言$schema 标识验证模式是否有效。描述另一个模式的模式称为“元模式”。

$schema适用于整个文档并且必须在根级别。它不适用于外部引用的 ( $ref, $recursiveRef) 文档。这些模式需要声明自己的 $schema.

如果$schema未使用,则实现可能允许您在外部指定一个值,或者它可能会假设应该使用哪个规范版本来评估模式。建议所有 JSON 模式都有一个$schema关键字来与读者和工具进行交流,以了解预期的规范版本。因此,大多数情况下,您会希望在架构的根目录下使用它:

"$schema": "https://json-schema.org/draft/2019-09/schema"

Draft 4 的标识符是http://json-schema.org/draft-04/schema#

Draft 4 定义了一个没有特定方言 ( http://json-schema.org/schema#)的$schema,这意味着使用最新的方言。这已被弃用,不应再使用。

您可能会遇到对 Draft 5 的引用。没有 JSON Schema 的 Draft 5 版本。Draft 5 指的是Draft 4 版本的无变化修订版。它不会添加、删除或更改任何功能。它只更新参考资料、进行澄清和修复错误。Draft 5 描述了DraftDraft4 版本。如果您来这里寻找有关Draft 5 的信息,您会在Draft 4 下找到它。我们不再使用“Draft”术语来指代补丁版本以避免这种混淆。

Draft 6 的标识符是http://json-schema.org/draft-06/schema#

Draft 7 的标识符是http://json-schema.org/draft-07/schema#

词汇表

2019-09 Draft中的新内容:文档即将推出

Draft4-7 在引入 Vocabularies 之前,您仍然可以使用自定义关键字扩展 JSON Schema,但该过程不太正式。您需要的第一件事是包含自定义关键字的元架构。最好的方法是为要扩展的版本制作元模式的副本,并对副本进行更改。您需要选择一个自定义 URI 来标识您的自定义版本。此 URI 不能是用于标识官方 JSON 架构规范Draft的 URI 之一,并且可能应该包含您拥有的域名。您可以将此 URI 与$schema关键字一起使用来声明您的模式使用您的自定义版本。

笔记并非所有实现都支持自定义元模式和自定义关键字实现。

指南

JSON Schema 的优势之一是它可以用 JSON 编写并在各种环境中使用。例如,它可用于前端和后端 HTML 表单验证。使用自定义词汇表的问题在于,您想要使用模式的每个环境都需要了解如何评估词汇表的关键字。元模式可用于确保模式编写正确,但每个实现都需要自定义代码来了解如何评估词汇表的关键字。

元数据关键字是最具互操作性的,因为它们不影响验证。例如,您可以添加units关键字。对于合规的验证器,将始终按预期生效。

{
"type": "number",
"units": "kg"
}
42 // OK
"42" // not OK

自定义关键字的下一个最佳候选者是不应用其他模式且不修改现有关键字行为的关键字。isEven关键字是一个例子,在某些语境下验证比没有验证要好,例如在浏览器中验证 HTML 表单时,此模式的性能将达到预期。完全验证仍然是需要的,并且应该使用可以理解自定义关键字的验证器。

{
"type": "integer",
"isEven": true
}
2 // OK
3 // OK,这通过因为验证器不理解 `isEven`
"3" // not OK,模式没有因为不理解 `isEven`而完全受损

互操作性最差的自定义关键字类型是应用其他模式或修改现有关键字行为的自定义关键字。一个例子就是requiredProperties,这个关键字声明属性并使它们成为必需属性。以下示例显示了在使用不理解自定义关键字的验证器进行校验时,模式如何变得几乎无用。这并不一定意味着这requiredProperties对关键字来说是个坏主意,只是说如果模式在不理解自定义关键字的上下文中使用时不是一个好的选择。

{
"type": "object",
"requiredProperties": {
"foo": { "type": "string"}
}
}
{ "foo": "bar" } // OK
{} // OK,因为`requiredProperties`不被理解
{ "foo": 42 } //ok,因为`requiredProperties`不被理解

构建复杂模式

在编写中等复杂度的计算机程序时,人们普遍认为,将程序“构建”为可复用的方法比到处复制粘贴重复的代码要好。同样在 JSON Schema 中,对于除最琐碎的模式之外,构建在很多地方可以复用的模式非常有用。本章将介绍可用于复用和构建模式的工具以及使用这些工具的一些实例。

模式识别

与任何其他代码一样,将模式分解为在必要时相互引用的逻辑单元,则模式更易于维护。为了引用模式,我们需要一种识别模式的方法。模式文档由非相对 URI 所标识。

模式文档不需要有标识符,但如果您想从另一个模式引用一个模式,则需要一个标识符。在本文档中,我们将没有标识符的模式称为“匿名模式”。

在以下部分中,我们将看到如何确定模式的“标识符”。

笔记

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

URI [1]非相对URI:含有模式的完整URI( https)。它可能包含一个 URI 片段 ( #foo)。有时本文档会使用“非相对 URI”来明确表示不允许使用相对 URI。

相对引用 [2]:不包含模式 ( https) 的部分 URI 。它可能包含一个片段 ( #foo)。

URI-引用 [3]:相对引用或非相对 URI。它可能包含一个 URI 片段 ( #foo)。

绝对 URI [4]包含模式 ( https) 但不包含 URI 片段 ( #foo) 的完整 URI 。

笔记 尽管模式由 URI 标识,但这些标识符不一定是网络可寻址的。它们只是标识符。通常,实现不会发出 HTTP 请求 ( https://) 或从文件系统 ( file://) 读取以获取模式。相反,它们提供了一种将模式加载到内部模式数据库中的方法。当模式被其 URI 标识符引用时,将从内部架构数据库中检索该模式。

JSON 指针

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

JSON 指针描述了一个以斜线分隔的路径来遍历文档中对象中的键。因此, /properties/street_address意味着:

  1. 找到键的值 properties
  2. 在该对象中,找到键的值 street_address

URI https://example.com/schemas/address#/properties/street_address 标识以下模式中子模式 { "type": "string" }

{ "$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关键字并在 URI 片段中使用该名称在模式中创建命名锚点。锚点必须以字母开头,后跟任意数量的字母、数字、-_:、 或.

在Draft 4 中,您以与Draft 6-7 中相同的方式声明锚点,$id只是只是id(没有美元符号)。

在Draft 6-7 中,使用$id仅包含 URI 片段的定义了命名锚点。URI 片段的值是锚点的名称。

JSON Schema 没有定义当$id同时包含片段和非片段 URI 部分时应该如何解析。因此,在设置命名锚点时,不应在 URI 引用中使用非片段 URI 部分。

笔记 如果一个命名的锚点在定义时不遵循这些命名规则,则它的行为未定义。您的锚点可能在某些实现中起作用,但在其他实现中不起作用。

URIhttps://example.com/schemas/address#street_address 标识以下模式的子模式

{"$anchor": "#street_address", "type": "string"}

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

基本 URI

使用非相对 URI 可能很麻烦,因此 JSON 模式中使用的任何 URI 都可以是 URI 引用,根据模式的基本 URI 进行解析,从而产生非相对 URI。本节介绍如何确定架构的基本 URI。

笔记 基本 URI 确定和相对引用解析由RFC-3986定义。如果您熟悉这在 HTML 中的工作原理,那么本节应该会感到非常熟悉。

检索 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 相同 https://example.com/schemas/address

$id

您可以在模式根目录中使用$id关键字来设置基本 URI 。$id的值是一个没有根据检索 URI解析片段的 URI 引用。生成的 URI 是模式的基本 URI。

Draft 4 中,$id只是id(没有$)。

Draft 4-7 中,允许在$id(或 Draft4中的id)中有片段。但是,设置包含 URI 片段的基本 URI 时的行为未定义,不应使用,因为实现可能会以不同方式对待它们。

笔记 这类似于<base> HTML 中标签

笔记$id关键字出现在子模式中时,它的含义略有不同。有关更多信息,请参阅捆绑部分。

让我们假设 URIhttps://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 时使用相对引用可能会出现问题。例如,我们不能将此模式用作匿名模式,因为没有检索 URI并且您无法解析相对引用。出于这个原因和其他原因,建议您在使用$id声明基本URI时尽量使用绝对URI.

无论检索 URI是什么 或者它是否用作匿名模式,以下模式的基本 URI 将始终是https://example.com/schemas/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"]}

$ref

一个模式可以使用$ref关键字引用另一个模式。$ref的值是根据模式的Base URI解析的 URI 引用。当获取$ref的值时,一个实现是使用解析的标识符来检索引用的模式并将该模式应用于实例中。

Draft 4-7 中,$ref表现略有不同。当一个对象包含一个$ref属性时,该对象被认为是一个引用,而不是一个模式。因此,您放入该对象的任何其他属性都不会被视为 JSON 模式关键字,并且会被验证器忽略。$ref只能在需要模式的地方使用。

在这个例子中,假设我们要定义一个客户记录,其中每个客户可能都有一个送货地址和一个账单地址。地址总是相同的结构(有街道地址、城市和州),所以我们不想在存储地址的所有地方都存储同一个模式。这不仅会使模式更加冗长,而且会使将来更新变得更加困难。如果这个公司将来从事国际业务,想为所有地址添加一个国家/地区字段,那么最好在一个地方而不是在使用地址的所有地方进行此操作。

{
"$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中的URI 引用根据模式的基本 URI ( https://example.com/schemas/customer)进行解析,结果为 https://example.com/schemas/address. 该实现检索该模式并使用它来获取“shipping_address”和“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"]
}

在/properties/shipping_address的ref无法解析到一个非相对URI,因此无法用于检索address模式。

$defs

有时,我们有一小段仅用于当前模式的子模式,将它们定义为单独的模式是没有意义的。虽然我们可以使用 JSON 指针或命名锚点来识别任何子模式,但$defs关键字为我们提供了一个标准化的位置来保存想在当前模式文档中复用的子模式。

让我们扩展之前的客户模式示例,以使用名称属性的通用架构。为此定义一个新模式没有意义,它只会在这个模式中使用,所以使用$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用描述性名称定义并在需要的地方引用。这允许模式的读者在深入研究更复杂的部分之前,更快速、更轻松地在高层次上理解模式。

笔记 可以引用外部子模式,但通常您希望将 a 限制$ref为引用外部模式或$defs.

递归

$ref关键字可以被用来创建一个自我递归模式。例如,您可能有一个person模式包含一个children的数组,每个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" }
}
}

扩展递归模式

2019-09 Draft中的新内容文档即将推出

捆绑

使用多个模式文档便于开发,但将所有模式捆绑到单个模式文档中通常更方便分发。这可以通过在子模式中使用id`在子模式中使用时,它表示嵌入式模式。

嵌入式模式的标识符是根据它出现在其中的模式的基本URI解析的得到的$id的值。包含嵌入模式的模式文档称为复合模式文档,复合架构文档中每个带有$id的模式称为模式资源。

Draft 4 中,$id只是id(没有$)。

Draft 4-7 中,子模式中的$id 不表示嵌入式模式。相反,它只是单模式文档中的基本 URI 更改。

这类似于HTML 中的 <iframe>标签

笔记 在开发模式时使用嵌入式模式是不常见的。通常最好不要显式使用此功能,并在需要时使用模式捆绑工具来构建捆绑模式。

此示例显示捆绑到复合模式文档中的客户模式示例和地址模式示例。

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

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

Draft 4-7 中,这两个 URI 都是有效的,因为子模式 $id仅表示基本 URI 更改,而不是嵌入模式。但是,虽然允许,仍然强烈建议 JSON 指针不要越过具有基本 URI 更改的模式。

您还应该看到"$ref": "#/definitions/state"解析为地址模式中的definitions关键字,而不是顶级模式中的关键字,就像未使用嵌入式模式时那样。

每个模式资源都是独立求值的,并且可能使用不同的 JSON 模式方言。上面的示例中, 地址模式资源使用了Draft 7 ,而客户模式资源使用 Draft 2019-09。如果嵌入式模式中没有声明$schema ,则默认使用父模式的方言。

Draft 4-7 中,子$id模式只是基本 URI 更改,不被视为独立的模式资源。因为$schema仅允许在模式资源的根目录中使用,所以使用子模式$id捆绑的所有模式必须使用相同的方言。