什么是 JSON Schema
JSON Schema 本身就是一个 JSON 文档,它用一种基于 JSON 的格式来定义其他 JSON 数据的结构和内容。它可以用来:
- 验证:确保 JSON 数据符合预期的格式(必填字段、数据类型、范围等)。
- 文档化:作为数据模型的清晰、机器可读的文档。
- 交互式验证:为前端表单提供动态校验规则。
- 自动化测试:验证 API 请求和响应的结构。
如何开始使用?
- 在线验证工具:访问 https://jsonschema.dev 或 https://www.jsonschemavalidator.net,可以直观地编写 Schema 和测试数据。
- 选择版本:在 Schema 根节点使用
"$schema"
关键字指定版本,如"https://json-schema.org/draft/2020-12/schema"
,这有助于编辑器和验证器提供正确的功能。 - 编程使用:几乎所有主流语言都有 JSON Schema 验证库(如 Python 的
jsonschema
, Java 的everit-org/json-schema
, JavaScript 的ajv
)。
核心概念与关键字
类别 | 关键字 | 用途 |
---|---|---|
核心 | $schema , $id , title , description |
标识 Schema 版本、提供元信息和文档 |
类型 | type |
定义基本数据类型 |
对象 | properties , required , additionalProperties |
定义对象结构、必填字段、允许额外字段 |
数组 | items , minItems , maxItems , uniqueItems |
定义数组元素、长度、唯一性 |
字符串 | minLength , maxLength , pattern , format |
定义字符串长度、格式、预定义格式 |
数值 | minimum , maximum , exclusiveMinimum , multipleOf |
定义数值范围、倍数 |
逻辑 | enum , const |
限制值为固定选项 |
组合 | allOf , anyOf , oneOf , not |
组合多个验证条件 |
条件 | if , then , else |
根据条件应用不同验证规则 |
复用 | $defs , $ref |
定义和引用可重用的子模式 |
类型约束:type
这是最核心的关键字,用于定义 JSON 数据的类型。基本类型有:string
, number
, integer
, boolean
, null
, object
, array
。
验证一个字符串
// Schema |
验证一个整数
// Schema |
对象属性约束:properties
, required
, additionalProperties
用于定义 JSON 对象的结构。
properties
: 定义对象中各个属性的 schema。required
: 定义一个字符串数组,列出必须存在的属性。additionalProperties
: 布尔值,定义是否允许出现properties
中未定义的额外属性。false
表示不允许。
定义一个用户对象
// Schema |
数组约束:items
, minItems
, maxItems
, uniqueItems
用于定义 JSON 数组的结构。
items
: 定义数组内每个元素的 schema。minItems
/maxItems
: 定义数组的最小/最大长度。uniqueItems
: 布尔值,定义数组元素是否必须全部唯一。
定义一个数字列表
// Schema |
字符串约束:minLength
, maxLength
, pattern
, format
基本使用
用于对字符串进行更细致的校验。
minLength
/maxLength
: 字符串的最小/最大长度。pattern
: 使用正则表达式验证字符串格式。format
: 使用预定义格式验证(如email
,date-time
,uri
,ipv4
等)。
定义用户名和邮箱
// Schema |
内置格式
具体参见 https://json-schema.fullstack.org.cn/understanding-json-schema/reference/string
以下是 JSON 模式规范中指定的格式列表。
日期和时间
日期和时间以 RFC 3339,第 5.6 节 的形式表示。这是日期格式的子集,也通常称为 ISO8601 格式。
"date-time"
:日期和时间组合在一起,例如,2018-11-13T20:20:39+00:00
。"time"
:草案 7 中新增时间,例如,20:20:39+00:00
"date"
:草案 7 中新增日期,例如,2018-11-13
"duration"
:2019-09 草案中新增由ISO 8601 ABNF for “duration”定义的持续时间。例如,P3D
表示持续时间为 3 天。
电子邮件地址
"email"
:互联网电子邮件地址,请参阅 RFC 5321,第 4.1.2 节。"idn-email"
:草案 7 中新增互联网电子邮件地址的国际化形式,请参阅RFC 6531。
主机名
"hostname"
:互联网主机名,请参阅 RFC 1123,第 2.1 节。"idn-hostname"
:草案 7 中新增国际化的互联网主机名,参见RFC5890,第 2.3.2.3 节。
IP 地址
"ipv4"
: IPv4 地址,根据 RFC 2673,第 3.2 节 中定义的点分十进制 ABNF 语法。"ipv6"
: IPv6 地址,根据 RFC 2373,第 2.2 节 定义。
资源标识符
"uuid"
:2019-09 草案中新增通用唯一标识符 (UUID),根据RFC 4122定义。例如:3e4666bf-d5e5-4aa7-b8ce-cefe41c7568a
"uri"
: 通用资源标识符 (URI),根据 RFC3986。"uri-reference"
:草案 6 中的新增内容URI 引用(URI 或相对引用),根据RFC3986,第 4.1 节。"iri"
:草案 7 中新增”uri” 的国际化等效项,根据RFC3987。"iri-reference"
:草案 7 中新增”uri-reference” 的国际化等效项,根据RFC3987如果模式中的值可以相对于特定的源路径(例如,来自网页的链接),通常最好使用"uri-reference"
(或"iri-reference"
)而不是"uri"
(或"iri"
)。"uri"
应仅在路径必须为绝对路径时使用。
URI 模板
"uri-template"
:草案 6 中的新增内容根据RFC6570的 URI 模板(任何级别)。如果您还不知道什么是 URI 模板,您可能不需要此值。
JSON 指针
"json-pointer"
:草案 6 中的新增内容JSON 指针,根据RFC6901。在构建复杂模式中,对 JSON 模式中使用 JSON 指针进行了更多讨论。请注意,这应仅在整个字符串仅包含 JSON 指针内容时使用,例如/foo/bar
。JSON 指针 URI 片段,例如#/foo/bar/
应使用"uri-reference"
。"relative-json-pointer"
:草案 7 中新增相对 JSON 指针。
正则表达式
"regex"
:草案 7 中新增正则表达式,应根据ECMA 262方言有效。
数字约束:minimum
, maximum
, exclusiveMinimum
, exclusiveMaximum
, multipleOf
用于对数字进行范围和小数精度校验。
minimum
/maximum
: 定义数值的下限/上限(包含等于)。exclusiveMinimum
/exclusiveMaximum
: 定义数值的下限/上限(不包含等于)。multipleOf
: 定义数值必须是某个数字的倍数。
定义商品价格和数量
// Schema |
枚举与常量:enum
, const
用于将值限制在一个固定的集合中。
enum
: 值必须是列表中的某一个。const
: 值必须严格等于这个常量。
定义订单状态
// Schema |
条件逻辑:if
, then
, else
允许你根据数据的某些条件来应用不同的验证规则。
条件验证 - 如果用户在国内,则邮编必填且格式为6位数字;如果在国外,则邮编可选
// Schema |
组合模式:allOf
, anyOf
, oneOf
, not
用于组合多个模式规则。
allOf
: 数据必须满足所有给定的模式。anyOf
: 数据必须满足至少一个给定的模式。oneOf
: 数据必须满足恰好一个给定的模式。not
: 数据必须不满足给定的模式。
组合模式
// Schema: 定义一个既是正数又是偶数的整数 |
结构组织:$defs
/ definitions
和 $ref
为了复用和模块化 Schema,可以使用引用。
$defs
(旧版草案中叫definitions
): 一个容器,用于在你的 Schema 中存放可重用的子模式。$ref
: 一个引用,指向$defs
中的某个子模式或外部 URL。
使用 $defs
和 $ref
复用模式
// Schema |
注释
JSON Schema 包含一些关键字,这些关键字不严格用于验证,而是用于描述模式的各个部分。这些“注释”关键字都不需要,但鼓励为了良好的实践使用它们,并可以使您的模式“自文档化”。
注释关键字可以在任何模式或子模式中使用。与其他关键字一样,它们只能使用一次。
The title
和 description
关键字必须是字符串。一个“标题”最好简短,而一个“描述”将提供关于模式所描述的数据目的的更详细的解释。
The default
关键字指定一个默认值。此值不用于在验证过程中填充缺失的值。非验证工具(例如文档生成器或表单生成器)可以使用此值来提示用户如何使用值。但是,default
通常用于表示如果一个值丢失,那么该值的语义与该值存在且为默认值时相同。The default
的值应该通过它所处的模式进行验证,但这不是必需的。
draft 6 中的新功能
The examples
关键字是一个提供验证模式的示例数组的地方。这不用于验证,但可能有助于向读者解释模式的效果和目的。每个条目都应该通过它所处的模式进行验证,但这并不是严格要求的。没有必要在 examples
数组中复制 default
值,因为 default
将被视为另一个示例。
draft 7 中的新功能
布尔关键字 readOnly
和 writeOnly
通常用于 API 上下文中。 readOnly
指示不应修改值。它可以用于指示更改值的 PUT
请求会导致 400 Bad Request
响应。 writeOnly
指示可以设置值,但它将保持隐藏状态。它可以用于指示您可以使用 PUT
请求设置值,但它不会包含在使用 GET
请求检索该记录时。
draft 2019-09 中的新功能
The deprecated
关键字是一个布尔值,表示该关键字适用的实例值不应使用,并且将来可能会被删除
{ |
组合模式:allOf
, anyOf
, oneOf
, not
详解
这些关键字都是基于布尔逻辑的:
- AND (
allOf
): 必须满足所有条件。 - OR (
anyOf
): 必须满足至少一个条件。 - XOR (
oneOf
): 必须满足恰好一个条件。 - NOT (
not
): 必须不满足这个条件。
它们接受一个数组作为值,数组中的每个元素都是一个独立的 schema 对象。
关键字 | 逻辑 | 描述 | 相当于 |
---|---|---|---|
allOf |
AND (∧) |
必须满足所有子模式 | schema1 ∧ schema2 ∧ ... |
anyOf |
OR (∨) |
必须满足至少一个子模式 | schema1 ∨ schema2 ∨ ... |
oneOf |
XOR (⊕) |
必须满足恰好一个子模式 | (schema1 ∧ ¬schema2) ∨ (¬schema1 ∧ schema2) |
not |
NOT (¬) |
必须不满足该子模式 | ¬schema |
重要提示:
oneOf
的陷阱:在使用oneOf
时,要特别注意确保子模式之间是互斥的,否则很容易出现一个数据同时匹配多个模式的情况导致验证失败。通常需要通过添加额外的约束(如const
或pattern
)来明确区分它们。- 性能:
anyOf
和oneOf
会按顺序尝试验证每个子模式,直到找到匹配项(anyOf
)或确定只有一个匹配项(oneOf
)。如果子模式很多或很复杂,可能会影响性能。 - 错误信息:当组合模式验证失败时,产生的错误信息可能比较难以理解,因为它需要解释复杂的逻辑关系。在设计 schema 时需要权衡复杂性和可维护性。
allOf
- 逻辑与(AND)
数据必须满足 所有 在 allOf
数组中提供的模式。这常用于组合多个约束或从多个来源继承属性。
用法: "allOf": [ {schema1}, {schema2}, ... ]
组合验证
创建一个 schema,要求数据是一个字符串,并且长度在 1 到 10 之间,同时必须全部大写。
// Schema |
组合对象(类似继承)
组合一个基础地址 schema 和一个包含额外字段的详细地址 schema。
// Schema |
anyOf
- 逻辑或(OR)
数据必须满足 anyOf
数组中提供的 至少一个 模式。这常用于创建联合类型或定义多种可接受的数据格式。
用法: "anyOf": [ {schema1}, {schema2}, ... ]
示例:支持多种类型或格式
验证一个字段可以是字符串形式的电话号码,也可以是数字形式的 ID。// Schema
{
"anyOf": [
{
"type": "string",
"pattern": "^\\d{3}-\\d{3}-\\d{4}$" // 匹配 123-456-7890
},
{
"type": "integer",
"minimum": 1000
}
]
}
// 有效数据 (满足第一个schema)
"555-123-4567"
// 有效数据 (满足第二个schema)
2000
// 无效数据 (两个schema都不满足)
"hello" // 不是正确的电话格式,也不是数字
500 // 是数字,但小于 minimum: 1000
oneOf
- 逻辑异或(XOR)
数据必须满足 oneOf
数组中提供的 恰好一个 模式。不能同时满足多个。这常用于枚举或确保数据只有一种明确的类型。
用法: "oneOf": [ {schema1}, {schema2}, ... ]
示例:精确的类型选择
验证一个值要么是字符串,要么是数字,但不能同时满足两者的验证规则(注意:一个数字字符串 "5"
同时是字符串且可以被解释为数字)。// Schema
{
"oneOf": [
{ "type": "string" },
{ "type": "number" }
]
}
// 有效数据 (只满足 string)
"Hello"
// 有效数据 (只满足 number)
42
// 无效数据 (同时满足两个!)
"5"
// 为什么?因为它既是 string (满足第一个schema),又可以被转换为数字 (满足第二个schema的 `type: number` 验证)。
// 要解决这个问题,需要让模式互斥,例如:
/*
{
"oneOf": [
{
"type": "string",
"pattern": "\\D" // 确保字符串包含非数字字符
},
{
"type": "number"
}
]
}
*/
// 这样 "5" 会因为不满足 pattern 而只匹配 number schema。
更实用的例子:支付方式
订单只能使用一种支付方式。// Schema
{
"type": "object",
"oneOf": [
{ // 信用卡支付
"properties": {
"paymentType": { "const": "credit_card" },
"cardNumber": { "type": "string" }
},
"required": ["paymentType", "cardNumber"]
},
{ // PayPal支付
"properties": {
"paymentType": { "const": "paypal" },
"email": { "type": "string", "format": "email" }
},
"required": ["paymentType", "email"]
}
]
}
// 有效数据 (只匹配信用卡模式)
{
"paymentType": "credit_card",
"cardNumber": "4111..."
}
// 有效数据 (只匹配PayPal模式)
{
"paymentType": "paypal",
"email": "user@example.com"
}
// 无效数据 (匹配了0个模式,缺少required字段)
{
"paymentType": "credit_card"
// 缺少 cardNumber
}
// 无效数据 (匹配了多个模式,这是不允许的)
{
"paymentType": "credit_card",
"cardNumber": "4111...",
"email": "user@example.com"
// 这个对象同时满足了两个模式定义的properties,违反了 oneOf
}
not
- 逻辑非(NOT)
数据必须 不满足 not
关键字后面提供的模式。这常用于排除某些特定的值或格式。
用法: "not": { schema }
示例:排除特定值
确保一个值不是空字符串。// Schema
{
"not": {
"type": "string",
"maxLength": 0
}
}
// 有效数据
"Hello"
42
["a"]
{"a": "b"}
// 无效数据
"" // 满足了 not 后面的schema(是字符串且长度<=0),所以被排除。
示例:确保不是某个枚举值
状态不能是 "deprecated"
。// Schema
{
"type": "string",
"not": {
"const": "deprecated"
}
}
// 有效数据
"active"
"inactive"
// 无效数据
"deprecated"
条件逻辑:if
, then
, else
详解
这三个关键字总是组合使用,其逻辑类似于编程语言中的 if-then-else
语句:
if
:- 这是一个条件判断。它的值是一个 schema 对象。
- 验证器会检查当前数据实例是否满足
if
这个 schema 中定义的规则。 - 注意:
if
条件本身的验证结果只用于决定执行路径,它不会导致整个验证失败。即使数据不满足if
条件,也只是导致then
被跳过,验证会继续检查else
(如果存在)。
then
:- 如果数据实例满足了
if
条件,那么验证器就会检查它是否也满足then
这个 schema 的规则。 - 如果此时数据不满足
then
的规则,验证就会失败。
- 如果数据实例满足了
else
:- (可选关键字)如果数据实例不满足
if
条件,那么验证器就会检查它是否满足else
这个 schema 的规则。 - 如果此时数据不满足
else
的规则,验证就会失败。详细示例说明
- (可选关键字)如果数据实例不满足
特别注意:
if
本身不贡献错误:数据即使不满足if
条件,也不会因此导致验证失败。失败只发生在数据满足了if
却不满足then
,或者不满足if
却不满足else
的情况下。else
是可选的:如果你只想在条件满足时施加额外约束,而在条件不满足时什么都不做,可以省略else
。- 条件可以是任何 Schema:
if
条件不仅可以检查简单的相等性(const
),还可以使用type
,pattern
,enum
, 甚至嵌套的allOf
/anyOf
等来构建复杂条件。 - 避免过度嵌套:虽然可以嵌套
if-then-else
(如示例1),但过度嵌套会使 Schema 难以理解和维护。有时使用oneOf
可能是更清晰的选择。 - 与组合模式结合:条件逻辑可以和你之前学到的
allOf
,anyOf
,not
等组合使用,构建出极其强大的验证逻辑(如示例2中then
里使用了not
)。
基本的字段依赖验证(经典用例)
场景:一个用户对象。如果 country
是 "USA"
,那么 zipCode
字段是必须的,并且必须匹配 5 位数字或 “ ZIP+4 ” 的格式。如果 country
是 "Canada"
,那么 postalCode
字段是必须的,并且必须匹配加拿大的邮政编码格式。对于其他国家,这两个字段都是可选的。
{ |
验证结果分析:
数据 | if 条件 | 应用规则 | 验证结果 | 原因 |
---|---|---|---|---|
{"country": "USA"} |
满足 | then |
无效 | 缺少必需的 zipCode 字段。 |
{"country": "USA", "zipCode": "12345"} |
满足 | then |
有效 | 满足 then 的所有规则。 |
{"country": "USA", "zipCode": "123"} |
满足 | then |
无效 | zipCode 格式不匹配 pattern 。 |
{"country": "Canada", "postalCode": "K1A 0B1"} |
不满足 | 外层的 else -> 内层的 then |
有效 | 满足内层 then 的规则。 |
{"country": "Germany"} |
不满足 | 外层的 else -> 内层的 else |
有效 | 内层 else 为空,无额外约束。 |
{"country": "Germany", "zipCode": "abc"} |
不满足 | 外层的 else -> 内层的 else |
有效 | 内层 else 为空,zipCode 存在与否和格式都不受约束。 |
验证互斥的字段组合
场景:一个支付对象。支付方式可以是信用卡(需要 cardNumber
)也可以是 PayPal(需要 email
)。但不能同时提供两者。
{ |
验证结果分析:
数据 | 验证结果 | 原因 |
---|---|---|
{"paymentMethod": "credit_card", "cardNumber": "4111..."} |
有效 | 满足 if ,应用 then :有 cardNumber 且无 email 。 |
{"paymentMethod": "paypal", "email": "a@b.c"} |
有效 | 不满足 if ,应用 else :有 email 且无 cardNumber 。 |
{"paymentMethod": "credit_card"} |
无效 | 满足 if ,应用 then :缺少必需的 cardNumber 。 |
{"paymentMethod": "credit_card", "cardNumber": "4111...", "email": "a@b.c"} |
无效 | 满足 if ,应用 then :但 then 中的 not 规则禁止了 email 字段的存在。 |
{"paymentMethod": "paypal"} |
无效 | 不满足 if ,应用 else :缺少必需的 email 。 |
验证数值范围依赖
场景:一个产品折扣对象。如果 discountType
是 "percentage"
(百分比),那么 value
必须在 0 到 100 之间。如果是 "fixed"
(固定金额),那么 value
必须大于 0。
{ |
验证结果分析:
数据 | 验证结果 | 原因 |
---|---|---|
{"discountType": "percentage", "value": 50} |
有效 | 满足 if ,应用 then :50 在 0-100 之间。 |
{"discountType": "percentage", "value": 150} |
无效 | 满足 if ,应用 then :150 > 100。 |
{"discountType": "fixed", "value": 30} |
有效 | 不满足 if ,应用 else :30 > 0。 |
{"discountType": "fixed", "value": 0} |
无效 | 不满足 if ,应用 else :0 不大于 0 (exclusiveMinimum )。 |
使用条件逻辑与组合模式验证多层嵌套对象
使用条件逻辑与组合模式验证多层嵌套对象
场景描述
假设我们正在构建一个电子商务平台的订单系统,需要验证订单数据的结构。订单对象具有以下复杂要求:
- 订单必须包含基本信息和至少一个商品项
- 根据支付方式不同,需要不同的验证规则:
- 信用卡支付需要卡号和安全码
- PayPal支付需要邮箱地址
- 银行转账需要银行账户信息
- 根据配送地址的国家不同,需要不同的邮编验证规则
- 某些商品类型有特殊要求:
- 数字商品需要电子邮件地址用于发送
- 危险品需要特殊处理标志和免责声明同意
- 订单总额必须等于所有商品价格加上运费和税费的总和
完整Schema示例
{ |
详细解释
基本结构验证
Schema 首先定义了订单对象必须包含的基本字段:
orderId
: 必须符合特定格式的字符串customer
: 包含姓名和邮箱的对象items
: 至少包含一个商品的数组shippingAddress
: 配送地址对象payment
: 支付信息对象totalAmount
: 订单总金额
商品项验证 (使用 oneOf
)
商品项使用 oneOf
来确保每种商品类型都有特定的必需字段:
- 实物商品: 需要
type
,productId
,name
,price
,quantity
- 数字商品: 需要
type
,productId
,name
,price
,licenseType
- 危险品: 需要
type
,productId
,name
,price
,quantity
,hazardLevel
配送地址验证 (使用嵌套 if
-then
-else
)
根据不同的国家,应用不同的邮编格式验证:
- 美国(US): 5位或5-4位数字格式
- 加拿大(CA): 字母数字交替格式
- 英国(UK): 英国特有的邮编格式
- 其他国家: 没有特定格式要求
支付方式验证 (使用 oneOf
)
支付方式使用 oneOf
确保每种支付方式都有正确的字段:
- 信用卡: 需要卡号、有效期和安全码
- PayPal: 需要邮箱地址
- 银行转账: 需要账户号、路由号和银行名称
条件验证 (使用 allOf
和 if
-then
)
使用 allOf
组合多个条件验证规则:
规则1: 如果订单包含数字商品,则客户必须提供有效的电子邮件地址{
"if": {
"properties": {
"items": {
"contains": {
"properties": {
"type": { "const": "digital" }
}
}
}
}
},
"then": {
"properties": {
"customer": {
"properties": {
"email": {
"type": "string",
"format": "email"
}
},
"required": ["email"]
}
}
}
}
规则2: 如果订单包含危险品,则必须接受免责声明{
"if": {
"properties": {
"items": {
"contains": {
"properties": {
"type": { "const": "hazardous" }
}
}
}
}
},
"then": {
"properties": {
"disclaimerAccepted": { "const": true }
},
"required": ["disclaimerAccepted"]
}
}
有效数据示例
包含数字商品的信用卡订单
{ |
包含危险品的银行转账订单
{ |
无效数据示例
缺少危险品免责声明
{ |
数字商品缺少客户邮箱
{ |
这个复杂的示例展示了如何结合使用 JSON Schema 的条件逻辑 (if
-then
-else
) 和组合模式 (allOf
, oneOf
) 来验证多层嵌套对象。关键点包括:
- 使用
oneOf
确保多种选项中的恰好一种被满足(如支付方式、商品类型) - 使用嵌套的
if
-then
-else
根据特定条件应用不同的验证规则(如根据国家验证邮编格式) - 使用
allOf
组合多个条件验证 确保所有相关规则都被应用(如数字商品需要邮箱、危险品需要免责声明) - 使用
contains
关键字 检查数组中是否存在特定类型的元素
这种组合使用使得 JSON Schema 能够表达极其复杂和精细的验证逻辑,几乎可以满足任何现实世界中的数据验证需求。
$schema
和$id
关键字
$schema
和 $id
是 JSON Schema 的元数据关键字,它们不是固定不变的字符串,但其作用和写法有明确的约定。
$schema
关键字
含义:$schema
关键字用于声明这个 JSON 文档本身遵循的是哪个版本的 JSON Schema 规范草案(Draft)。它就像是 XML 中的 DOCTYPE
声明或 HTML 中的 `` 声明。
为什么需要它?
- 版本控制:JSON Schema 规范本身在不断发展,有不同的草案版本(如 Draft-07, Draft-2019-09, Draft-2020-12)。每个版本都可能引入新的关键字或修改现有关键字的行为。使用
$schema
明确告知验证器应该使用哪一套规则来解析这个 schema。 - 工具链支持:许多编辑器(如 VS Code、IntelliJ IDEA)和验证器依赖这个值来提供自动完成、语法高亮和实时验证。如果没有它,这些工具可能不知道如何正确处理你的 schema 文件。
常用值(不是固定写法,但必须从以下中选一个):
值 | 对应的草案版本 | 说明 |
---|---|---|
"http://json-schema.org/draft-07/schema#" |
Draft-07 | 较旧的版本,但仍被广泛使用。 |
"http://json-schema.org/draft/2019-09/schema" |
Draft-2019-09 | 引入了 unevaluatedProperties 等新关键字。 |
"https://json-schema.org/draft/2020-12/schema" |
Draft-2020-12 | 当前推荐的最新稳定版本。 |
结论:虽然值不是固定的,但强烈建议总是在 schema 的根节点包含 $schema
关键字,并指定为你所使用的草案版本URL。这被认为是最佳实践。
$id
关键字
含义:$id
为 schema 定义一个统一资源标识符(URI)。这个 URI 是 schema 的唯一标识符,可以理解为它的“命名空间”或“基础地址”。
它有两个主要作用:
唯一标识(作为引用名称)
这是它的核心作用。当其他 schema 想要引用(使用 $ref
) 当前这个 schema 或其内部定义时,就会使用这个 $id
作为基址。
例如,你在一个 schema 中定义了:{
"$id": "https://example.com/schemas/address.json",
"$defs": {
"street": { "type": "string" }
}
}
在另一个 schema 中,你可以这样引用它:{
"type": "object",
"properties": {
"homeAddress": {
"$ref": "https://example.com/schemas/address.json#/$defs/street"
},
"workAddress": {
"$ref": "https://example.com/schemas/address.json"
}
}
}
这里的 $ref
值就是基于 $id
提供的基址进行构建的。
解析相对引用(作为基础URI)
在 schema 内部,如果使用相对路径进行引用(例如 "$ref": "#/$defs/myDefinition"
),这个相对路径是相对于 $id
所定义的基址来解析的。
关于它的值:
- 可以是任何 URI:它不一定需要是一个真实可访问的网址。它只是一个标识符。常用的格式是
https://你的域名/schemas/文件名.json
。 - 推荐使用绝对 URI:为了确保唯一性和可移植性,最好使用完整的绝对 URI(如
https://...
),而不是相对路径(如"/schemas/address.json"
)。 - 在根节点定义:
$id
通常在 schema 的根节点定义,为整个文档设置基础 URI。但它也可以在其他位置出现,用于修改局部的基础 URI(高级用法)。
总结与对比
关键字 | 作用 | 必要性 | 值示例 |
---|---|---|---|
$schema |
定义本schema遵循的规范版本 | 强烈推荐 | "https://json-schema.org/draft/2020-12/schema" |
$id |
为本schema定义一个唯一标识符(URI),供外部或内部引用 | 推荐(尤其在schema需要被复用时) | "https://example.com/schemas/address.json" |
所以,你给出的例子:
{ |
含义是:
- “本 Schema 文档遵循 JSON Schema Draft-2020-12 版本的规范。”
- “本 Schema 的唯一标识符是
https://example.com/complex-order-schema.json
。其他 Schema 可以通过这个 URI 来引用我,我内部的相对引用路径也基于这个 URI 进行解析。”
特别注意:
- 始终包含
$schema
。 - 如果你的 Schema 会被其他 Schema 通过
$ref
引用,或者你想清晰地命名它,那么就应该包含$id
。 - 如果你只是写一个简单的、一次性的、不被复用的 Schema,
$id
可以省略。
JSON Schema 中的唯一性约束
在 JSON Schema 中,唯一性约束主要通过 uniqueItems
关键字来实现,但它有一个重要的限制:它只适用于数组(array)类型,用于确保数组中的所有元素都是唯一的。
JSON Schema 没有提供类似于数据库中的 UNIQUE
约束的关键字来直接保证整个文档中某个字段的唯一性(例如,确保所有用户对象的 email
字段值都是唯一的)。这种全局唯一性需要在应用层或数据库层实现。
数组元素的唯一性约束 (uniqueItems
)
作用:当 uniqueItems
设置为 true
时,要求数组中的每个元素都是唯一的。
工作原理:验证器会使用深度比较(deep comparison)来检查数组元素是否重复。对于对象,会比较所有属性及其值;对于数组,会按顺序比较每个元素。
针对不同数据类型的示例:
基本数据类型数组
// Schema |
字符串数组
// Schema |
布尔值数组
// Schema |
对象数组 - 基于整个对象的唯一性
// Schema |
嵌套结构数组
// Schema |
针对对象特定字段的唯一性约束
这是更复杂但更常见的需求:确保数组中每个对象的某个特定字段(或一组字段)的值是唯一的。
JSON Schema 没有直接的关键字来实现这一点,但可以通过组合使用 uniqueItems
和其他技术来模拟这种效果。
使用枚举和大小限制
这种方法适用于已知所有可能值的情况。// Schema: 确保角色名称唯一
{
"type": "array",
"items": {
"type": "object",
"properties": {
"roleName": {
"type": "string",
"enum": ["admin", "editor", "viewer", "guest"] // 预定义唯一值
}
},
"required": ["roleName"]
},
"maxItems": 4 // 不能超过枚举值的数量
}
// 有效数据
[
{ "roleName": "admin" },
{ "roleName": "editor" }
]
// 无效数据(通过 maxItems 间接防止重复)
[
{ "roleName": "admin" },
{ "roleName": "editor" },
{ "roleName": "viewer" },
{ "roleName": "guest" },
{ "roleName": "admin" } // 虽然能通过enum检查,但会被maxItems阻止
]
使用组合模式实现真正基于字段的唯一性
这是一种更强大的方法,使用 allOf
和 not
的组合来验证唯一性。
{ |
更精确的实现方式:
实际上,更准确的方法是使用应用逻辑或自定义验证,因为标准的 JSON Schema 无法直接实现基于字段的唯一性。但在某些验证器中,你可以这样模拟:
{ |
针对多个字段组合的唯一性约束
如果需要确保多个字段的组合是唯一的,可以使用类似的方法:
{ |
实际应用建议
- 对于简单唯一性(整个数组元素唯一):使用
uniqueItems: true
对于字段级唯一性:
- 如果可能值已知且有限:使用
enum
+maxItems
- 对于一般情况:在应用层实现验证逻辑
- 对于需要严格验证的场景:考虑使用数据库的唯一约束
- 如果可能值已知且有限:使用
性能考虑:
uniqueItems
需要对数组进行 O(n²) 的比较,对于大型数组可能影响性能。
总结
约束类型 | JSON Schema 支持 | 实现方法 |
---|---|---|
数组元素整体唯一 | ✅ 直接支持 | uniqueItems: true |
对象特定字段唯一 | ❌ 不直接支持 | 应用层逻辑,或使用 enum + maxItems 间接实现 |
多字段组合唯一 | ❌ 不直接支持 | 应用层逻辑 |
JSON Schema 与 Go 校验实现
增强后的 JSON Schema
{ |
增强的 Go 语言校验实现
package main |
主要增强内容
clientID 唯一性约束:
- 在 JSON Schema 中添加了
minimum: 1
约束,确保 clientID 为正整数 - 添加了
validateClientIDUnique
自定义验证函数,检查所有 shapes 和 tags 中的 clientID 是否唯一 - 该函数会记录重复的 clientID 及其位置信息
- 在 JSON Schema 中添加了
增强的错误报告:
- 改进了错误消息,明确指出哪个 clientID 重复以及在哪个位置
- 区分了 shapes 和 tags 中的重复情况
测试用例:
- 添加了
testDuplicateClientID
函数,用于测试 clientID 重复的情况 - 这有助于验证自定义验证函数的正确性
- 添加了
结构化验证流程:
- 将验证流程分为三个部分:
- JSON Schema 验证
- Points 相关验证(偶数个元素、非负值)
- ClientID 唯一性验证
- 将验证流程分为三个部分:
使用说明
首先安装所需的Go依赖:
go get github.com/xeipuuv/gojsonschema
将上述代码保存为
validate.go
文件运行程序:
go run validate.go
程序将输出验证结果,包括:
- JSON Schema 验证结果
- Points 验证结果(偶数个元素、非负值)
- ClientID 唯一性验证结果
- ClientID 重复测试结果
注意事项
- 示例JSON数据中的 clientID 已经确保唯一(1-7)
- 测试函数中特意创建了一个重复的 clientID 用于验证
- 所有验证错误都会以清晰的方式报告,包括重复的 clientID 值和位置信息
- clientID 现在必须是正整数(最小值1)
使用 Java 实现 JSON Schema 校验
下面我将展示如何使用 Java 实现与之前 Go 代码相同的 JSON Schema 校验功能,包括对 points
属性的约束和 clientID
唯一性检查。
添加 Maven 依赖
首先,在 pom.xml
中添加必要的依赖:
<dependencies> |
JSON Schema 文件
创建 schema.json
文件放在资源目录下 (src/main/resources/schema.json
):
{ |
Java 实现代码
创建 JsonValidator.java
类:
package com.example.validator; |
单元测试
创建 JsonValidatorTest.java
测试类:
package com.example.validator; |
使用说明
项目设置:
- 创建一个 Maven 项目
- 将上述代码添加到相应的包中
- 将 JSON Schema 文件放在
src/main/resources/schema.json
运行验证:
- 可以直接运行
JsonValidator
类的main
方法进行测试 - 也可以运行单元测试来验证各种情况
- 可以直接运行
API 使用:
JsonValidator validator = new JsonValidator();
String jsonString = "..."; // 你的 JSON 字符串
JsonValidator.ValidationResult result = validator.validate(jsonString);
if (result.isValid()) {
System.out.println("验证通过");
} else {
result.printResults(); // 打印所有错误信息
}
功能说明
这个 Java 实现提供了与之前 Go 代码相同的功能:
- JSON Schema 验证:使用 NetworkNT 的 JSON Schema 验证器验证基本结构和类型
- Points 验证:
- 检查 points 数组元素个数是否为偶数
- 检查 points 数组元素值是否都大于等于 0
- ClientID 唯一性验证:检查所有 shapes 和 tags 中的 clientID 是否唯一
- 综合验证:提供统一的验证接口,返回详细的验证结果
这个实现能够有效验证您的 JSON 数据是否符合所有预期的约束条件,确保数据的一致性和完整性。
参考文档
- https://json-schema.fullstack.org.cn/learn/getting-started-step-by-step
- https://json-schema.org/
- https://json-schema.fullstack.org.cn/specification
- https://json-schema.fullstack.org.cn/draft/2020-12/json-schema-core