4、声明式校验规则:JSON Schema、YAML规则定义、注解式校验

前面我们聊了硬编码校验和规则引擎,说白了都是「命令式」——你得告诉程序每一步怎么做。这一章换个思路,聊聊「声明式」。你只管说数据长什么样,校验逻辑由框架自动推导。我个人习惯,只要项目允许,优先用声明式。为什么?因为可读性高、维护成本低,而且容易和文档对齐。

4.1 声明式校验的核心思想

声明式校验,核心就一句话:描述约束,而不是实现逻辑。你定义「年龄必须在18到60之间」,框架自动生成校验代码。你不需要写if-else,不需要考虑异常处理,甚至不需要关心校验顺序。

我在项目中遇到过好几次这样的场景:业务方拿着需求文档说「这个字段不能为空,那个字段必须是邮箱格式」。如果用命令式,我得写一堆if语句,然后单元测试覆盖每个分支。用声明式呢?直接写一个Schema文件,业务方自己都能看懂。说白了,声明式把「校验逻辑」和「业务代码」解耦了。

声明式 vs 命令式 对比
维度 声明式 命令式
关注点 数据应该长什么样 如何一步步检查
可读性 高,非技术人员也能看懂 低,需要理解代码逻辑
维护成本 低,改约束即可 高,改逻辑可能影响其他部分
复用性 强,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 声明式校验的架构位置

在实际项目中,声明式校验通常放在哪里?我画了一张图来说明:

声明式校验在架构中的位置 客户端请求 API网关(JSON Schema校验) 应用层(注解式校验) 业务服务(YAML规则引擎) 数据持久层(数据库约束) 校验层级说明 • 网关层:格式校验 • 应用层:参数校验 • 服务层:业务规则 • 数据层:完整性约束 越早拦截,成本越低

从这张图可以看出,声明式校验可以贯穿整个请求链路。我个人建议:在网关层用JSON Schema做第一道防线,在应用层用注解做第二道防线,在服务层用YAML规则做复杂业务校验。三层校验互不冲突,反而能形成互补。

4.6 实战建议:如何选择

说了这么多,到底该怎么选?我总结几条经验:

  1. 对外接口:优先用JSON Schema。因为它是标准,客户端可以提前校验,减少无效请求。
  2. 内部配置:用YAML规则定义。可读性好,方便运维人员修改。
  3. Java项目:注解式校验是标配。配合Spring Boot的@Validated,几乎零配置。
  4. 混合使用:不冲突。比如注解校验基本格式,YAML规则校验业务逻辑。
一个小技巧
如果你用JSON Schema,建议配合ajv(JavaScript)或everit-json-schema(Java)这类高性能校验器。它们支持异步校验和自定义关键字,能覆盖大部分复杂场景。