4、声明式校验规则:JSON Schema、YAML规则定义、注解式校验
前面我们聊了硬编码校验和规则引擎,说白了都是「命令式」——你得告诉程序每一步怎么做。这一章换个思路,聊聊「声明式」。你只管说数据长什么样,校验逻辑由框架自动推导。我个人习惯,只要项目允许,优先用声明式。为什么?因为可读性高、维护成本低,而且容易和文档对齐。
4.1 声明式校验的核心思想
声明式校验,核心就一句话:描述约束,而不是实现逻辑。你定义「年龄必须在18到60之间」,框架自动生成校验代码。你不需要写if-else,不需要考虑异常处理,甚至不需要关心校验顺序。
我在项目中遇到过好几次这样的场景:业务方拿着需求文档说「这个字段不能为空,那个字段必须是邮箱格式」。如果用命令式,我得写一堆if语句,然后单元测试覆盖每个分支。用声明式呢?直接写一个Schema文件,业务方自己都能看懂。说白了,声明式把「校验逻辑」和「业务代码」解耦了。
| 维度 | 声明式 | 命令式 |
|---|---|---|
| 关注点 | 数据应该长什么样 | 如何一步步检查 |
| 可读性 | 高,非技术人员也能看懂 | 低,需要理解代码逻辑 |
| 维护成本 | 低,改约束即可 | 高,改逻辑可能影响其他部分 |
| 复用性 | 强,Schema可跨语言使用 | 弱,通常绑定特定语言 |
4.2 JSON Schema:最通用的声明式校验标准
JSON Schema是目前最广泛使用的声明式校验规范。它不依赖任何编程语言,你可以在Java、Python、JavaScript、Go等任何语言中使用对应的校验库。我最早接触JSON Schema是在做微服务网关的时候,用来校验API请求参数。
一个典型的JSON Schema长这样:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "用户注册请求",
"type": "object",
"properties": {
"username": {
"type": "string",
"minLength": 3,
"maxLength": 20,
"pattern": "^[a-zA-Z0-9_]+$"
},
"email": {
"type": "string",
"format": "email"
},
"age": {
"type": "integer",
"minimum": 18,
"maximum": 60
},
"tags": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
}
},
"required": ["username", "email", "age"]
}
嗯,这里要注意几个关键点:
- type:定义字段类型,支持string、number、integer、boolean、array、object、null
- format:内置的格式校验,比如email、date-time、uri、ipv4等
- pattern:正则表达式,用于字符串格式校验
- required:必填字段列表
- additionalProperties:是否允许出现未定义的字段,默认是true,建议显式设为false
我曾经在项目中遇到一个线上问题:某个接口的JSON Schema没有设置
additionalProperties: false,结果前端传了一个多余的字段,后端没报错,但那个字段被错误地写入了数据库。从那以后,我要求所有对外接口的Schema都必须显式关闭额外属性。
4.3 YAML规则定义:更简洁的声明式表达
JSON Schema虽然强大,但JSON语法本身比较啰嗦。YAML作为JSON的超集,语法更简洁,适合手写规则定义。你想想看,一个复杂的嵌套Schema,用JSON写可能几百行,用YAML可能一百行就搞定了。
下面是一个YAML格式的校验规则定义:
# 订单创建请求校验规则
order:
type: object
required:
- orderId
- items
- totalAmount
properties:
orderId:
type: string
pattern: '^ORD\d{12}$'
description: "订单号,格式:ORD+12位数字"
items:
type: array
minItems: 1
maxItems: 50
items:
type: object
required:
- sku
- quantity
- price
properties:
sku:
type: string
pattern: '^[A-Z]{2}\d{6}$'
quantity:
type: integer
minimum: 1
maximum: 999
price:
type: number
exclusiveMinimum: 0
totalAmount:
type: number
exclusiveMinimum: 0
discount:
type: number
minimum: 0
maximum: 1
default: 0
YAML规则定义有几个好处:
- 注释友好:可以用#写注释,JSON不行
- 层级清晰:缩进代替了花括号,视觉上更直观
- 支持锚点:可以用
&和*复用规则,减少重复
在团队协作中,我通常用YAML写规则定义,然后通过工具自动转换成JSON Schema。这样既保留了YAML的可读性,又能兼容JSON Schema的生态工具链。
4.4 注解式校验:代码即文档
注解式校验是声明式校验在代码层面的体现。你直接在字段上打注解,框架在运行时自动拦截并校验。Java生态中最典型的就是Jakarta Bean Validation(以前叫JSR 380)。
看一个例子:
public class UserCreateRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度3-20位")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;
@NotBlank
@Email(message = "邮箱格式不正确")
private String email;
@NotNull
@Min(value = 18, message = "年龄不能小于18岁")
@Max(value = 60, message = "年龄不能大于60岁")
private Integer age;
@NotEmpty
@Size(min = 1, max = 5)
private List<@NotBlank String> tags;
// getters and setters
}
注解式校验的好处很明显:
- 零侵入:校验逻辑和业务逻辑完全分离
- 编译期检查:注解写错了IDE会报错
- 支持分组校验:比如创建和更新用不同的校验规则
- 可自定义:可以写自己的校验注解
为什么会有人觉得注解式校验不够灵活?因为有些复杂校验确实不好用注解表达。比如「如果A字段不为空,则B字段必须大于C字段」。这种跨字段校验,我建议用自定义注解或者结合方法级别的@ScriptAssert。
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| JSON Schema | API接口、配置文件、跨语言数据交换 | 语言无关、生态成熟、支持工具生成文档 | JSON语法冗余、不支持复杂逻辑 |
| YAML规则定义 | 配置文件、规则模板、团队协作 | 语法简洁、支持注释和锚点 | 缩进敏感、工具链不如JSON Schema丰富 |
| 注解式校验 | Java/Spring项目、ORM实体校验 | 编译期检查、IDE友好、与框架深度集成 | 绑定语言、跨字段校验困难 |
4.5 声明式校验的架构位置
在实际项目中,声明式校验通常放在哪里?我画了一张图来说明:
从这张图可以看出,声明式校验可以贯穿整个请求链路。我个人建议:在网关层用JSON Schema做第一道防线,在应用层用注解做第二道防线,在服务层用YAML规则做复杂业务校验。三层校验互不冲突,反而能形成互补。
4.6 实战建议:如何选择
说了这么多,到底该怎么选?我总结几条经验:
- 对外接口:优先用JSON Schema。因为它是标准,客户端可以提前校验,减少无效请求。
- 内部配置:用YAML规则定义。可读性好,方便运维人员修改。
- Java项目:注解式校验是标配。配合Spring Boot的
@Validated,几乎零配置。 - 混合使用:不冲突。比如注解校验基本格式,YAML规则校验业务逻辑。
如果你用JSON Schema,建议配合
ajv(JavaScript)或everit-json-schema(Java)这类高性能校验器。它们支持异步校验和自定义关键字,能覆盖大部分复杂场景。