Go jsonschema 的 format 为什么不总是强制断言
来源:17golang原创
时间:2026-10-30 21:31:49 188浏览 收藏
很多 Go 服务已经在 JSON Schema 中写了 format: email、format: uuid 或自定义格式,却发现不符合格式的实例没有被拒绝。对 github.com/santhosh-tekuri/jsonschema/v5 来说,关键不在于 format 拼写错误,而在于它在不同草案中可能只是注解,不一定自动成为失败条件。
结论:生产配置要同时明确 $schema,并在支持的草案下设置 Compiler.AssertFormat = true。如果只写 schema 中的 format,却没有打开断言开关,应用就可能只得到格式信息,而不是验证错误。
官方地址:https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v5

先区分 format 注解与 format 断言
JSON Schema 的 format 既可以表达“这个值应该具有什么语义”,也可以参与“这个值不满足就失败”的断言。两者看起来只差一个配置项,运行效果却不同:
- 注解:给工具或后续处理阶段提供格式信息,当前验证不一定因为格式不匹配失败。
- 断言:格式检查返回失败,
Validate返回ValidationError。
该库的文档明确说明,draft 2019-09 及以上可以通过 Compiler.AssertFormat 开启 format assertion。因此,下面三件事必须一起看:schema 使用的草案、编译器开关、实例本身是否先满足 type。
固定草案并建立编译器
不要让生产环境依赖“缺少 $schema 时库当前默认的草案”。库会在缺少字段时使用当前实现的最新草案,但依赖默认值会让升级后的行为边界变得不明显。更稳妥的做法是在 schema 中写明草案,再由 Go 编译器按这个 schema 编译。
package main
import (
"encoding/json"
"fmt"
"log"
"strings"
"github.com/santhosh-tekuri/jsonschema/v5"
)
func main() {
// 显式声明草案,避免库升级后依赖隐含的默认草案。
schemaText := `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"email": {"type": "string", "format": "email"}
},
"required": ["email"]
}`
compiler := jsonschema.NewCompiler()
// 先加入资源,再按资源名编译,便于后续替换为文件或远程加载器。
if err := compiler.AddResource("account.json", strings.NewReader(schemaText)); err != nil {
log.Fatalf("加入 schema 失败: %v", err)
}
schema, err := compiler.Compile("account.json")
if err != nil {
log.Fatalf("编译 schema 失败: %v", err)
}
var document any
// 保持原始 JSON 类型,后续 Validate 才能按 JSON 值检查。
if err := json.Unmarshal([]byte(`{"email":"not-an-email"}`), &document); err != nil {
log.Fatalf("解析实例失败: %v", err)
}
if err := schema.Validate(document); err != nil {
fmt.Printf("验证失败: %v\n", err)
}
}
这段代码有一个故意留下的对照点:schema 已经有 format,但编译器还没有设置 AssertFormat。它适合用来说明问题,不应直接作为生产配置。生产版要在创建编译器后补上断言开关。
显式开启格式断言
在 draft 2019-09 或更高草案下,把 AssertFormat 设为 true,再调用同一个 Validate。最小改动如下:
compiler := jsonschema.NewCompiler()
// 让支持的草案把 format 失败转成验证错误。
compiler.AssertFormat = true
if err := compiler.AddResource("account.json", strings.NewReader(schemaText)); err != nil {
log.Fatalf("加入 schema 失败: %v", err)
}
schema, err := compiler.Compile("account.json")
if err != nil {
log.Fatalf("编译 schema 失败: %v", err)
}
// document 应该是 json.Unmarshal 得到的原始 JSON 值。
if err := schema.Validate(document); err != nil {
// 生产日志至少保留 schema、实例路径和错误文本。
log.Printf("schema=account.json validation_error=%v", err)
}
注意开关作用于编译器,而不是某一次 Validate 调用。已经编译好的 schema 不会因为之后修改另一个编译器的配置而改变,所以建议把“创建编译器、注册格式、编译 schema”封装成一个初始化单元。
把内置格式与自定义格式分开
内置格式由库提供检查函数;业务系统也可以把函数放入 compiler.Formats。自定义格式应该有清晰的名称、输入类型和失败策略,不能只在文档中写一个名字,然后期待库自动理解业务规则。
compiler := jsonschema.NewCompiler()
// 只有在显式断言打开时,自定义格式失败才会影响 Validate 结果。
compiler.AssertFormat = true
compiler.Formats["company-code"] = func(value interface{}) bool {
text, ok := value.(string)
if !ok {
// 非字符串交给 type 关键字处理,避免格式函数把类型问题吞掉。
return true
}
// 业务格式:ACME- 加 6 位数字;实际规则应与接口契约保持一致。
if len(text) != 11 || !strings.HasPrefix(text, "ACME-") {
return false
}
for _, ch := range text[5:] {
if ch '9' {
return false
}
}
return true
}
schemaText := `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
"format": "company-code"
}`
这里仍然保留 type: string,因为 format 不应该替代类型约束。自定义函数对非字符串返回 true,是为了让类型错误由 type 产生;如果格式函数把所有类型都当成失败,错误信息会混在一起,排查时很难区分输入结构问题和格式问题。

把验证错误记录成可审计事件
生产系统不要只记录一句“校验失败”。至少要区分以下三类:
| 类别 | 典型位置 | 处理建议 |
|---|---|---|
| schema 编译失败 | Compile | 阻断发布或阻断加载新 schema |
| 类型/结构失败 | type、required、properties | 返回输入结构错误,指导调用方修正字段 |
| 格式断言失败 | format | 返回字段格式错误,并保留实例路径 |
ValidationError 提供关键字位置、实例位置和错误消息等信息。日志可以保留 schema 标识、关键字路径、实例路径和请求追踪号,但不要把完整敏感实例直接写入日志。
上线前的五项检查
- schema 是否写了明确的
$schema,而不是依赖库默认草案。 - 编译器是否在注册和编译前设置了
AssertFormat = true。 - 每个格式字段是否同时写了正确的
type。 - 自定义格式是否登记在同一个编译器实例,并有非目标类型的处理策略。
- 错误日志是否能区分 schema 编译失败、结构失败和格式失败。
如果只记住一句话,就是:format 写在 schema 里,不代表它已经成为失败条件;草案、AssertFormat 和 type 三者要一起配置。
常见问题
为什么写了 format=email,错误邮箱仍然通过?
优先检查草案和 AssertFormat。在支持 format assertion 的草案下没有打开断言时,format 可能只作为注解保留。
只设置 AssertFormat,不写 $schema 可以吗?
不建议。缺少 $schema 时会依赖库当前实现的默认草案,升级依赖后行为边界不够清晰。
自定义格式函数应该在类型不匹配时返回 false 吗?
通常让 type 负责类型错误,格式函数只负责自己的语义检查;这样错误分类更清楚。具体策略应与接口契约保持一致。
参考资料
https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v5:jsonschema/v5 包文档,包含 AssertFormat、Formats 与示例。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
184 收藏
-
229 收藏
-
164 收藏
-
308 收藏
-
393 收藏
-
420 收藏
-
166 收藏
-
319 收藏
-
434 收藏
-
348 收藏
-
414 收藏
-
266 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习