用 JSON Schema 做接口校验:从原理到在 Spring Boot 中的落地实践
适用读者:熟悉 Spring Boot / REST API,有过
@Valid
(Bean Validation)经验,想把校验规则配置化、跨语言复用、支持复杂条件。
目录
- 为什么要用 JSON Schema(对比
@Valid
) - JSON Schema 核心原理(你至少该知道的那部分)
- 在 Spring Boot 中的三种集成方式
3.1 代码内置 Schema(最简单)
3.2 资源文件/远程加载(团队协作友好)
3.3 数据库存储 + 动态热加载(高扩展) - 可复用的“Schema 注册中心”与缓存设计
- 控制器入参校验的落地:拦截、报错、返回体规范
- 复杂场景示例(if/then/else、oneOf、数组约束、$ref 组合)
- 与
@Valid
的协同:分层职责与迁移策略 - 性能与稳定性建议(预编译、缓存、限流、格式语义)
- 运维与治理:版本化、灰度、回滚、可观测性
- 附录:完整代码片段汇总
1. 为什么要用 JSON Schema(对比 @Valid
)
@Valid
(Hibernate Validator)优点是直观、与 Java 深度集成,但它强绑定语言与代码,规则改动往往要发版;描述跨字段条件也不够灵活。
JSON Schema 的优势:
- 跨语言复用:一份 Schema,Java/Node/Python 同时用。
- 配置化/动态化:把规则放到资源库或数据库,无需改代码即可调整。
- 表达力强:
if/then/else
、oneOf
、allOf
、数组/对象深度约束、格式(email、uuid、date-time)等。 - 前后端统一:前端表单校验、Mock、文档生成都可复用同一份 Schema。
典型使用场景:低代码/表单系统、报表/检索条件、可配置化业务参数,或需要对外生态(跨语言 SDK)的平台级接口。
2. JSON Schema 核心原理
Schema 本体:一份 JSON,用来描述另一个 JSON的结构与约束。
关键字段:
$schema
:声明使用的草案版本(建议选draft-07
或更高)。type
/properties
/required
/items
/enum
/format
/pattern
/minimum
等。- 组合与条件:
allOf
、anyOf
、oneOf
、not
、if
/then
/else
。 - 复用:
$id
+$ref
支持跨文件引用。
验证过程:
JSON
→ 解析为JsonNode
→ 调用验证器与JsonSchema
对比 → 返回错误列表(路径 + 失败原因)。
3. 在 Spring Boot 中的三种集成方式
下文示例均基于 Java + Spring Boot +
com.networknt:json-schema-validator
(选择最新版稳定版)。该库 API 清晰,支持较新草案;如果你已有 Everit 也可替换,设计相同。
3.1 代码内置 Schema(最简单)
适合:规则较少、变动不频繁的内部接口。
Maven 依赖(示意)
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version><!-- 选用最新稳定版 --></version>
</dependency>
Controller 直接校验
@RestController
@RequestMapping("/orders")
public class OrderController {
private final ObjectMapper mapper = new ObjectMapper();
private final JsonSchemaFactory factory =
JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); // 或 draft-07
// 示例:下方 schema 文本可以提到配置文件/DB,这里为简化直接内嵌
private static final String ORDER_CREATE_SCHEMA = """
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CreateOrder",
"type": "object",
"properties": {
"type": { "enum": ["physical", "virtual"] },
"userId": { "type": "string", "minLength": 1 },
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"sku": { "type": "string", "minLength": 1 },
"qty": { "type": "integer", "minimum": 1 },
"price": { "type": "number", "minimum": 0 }
},
"required": ["sku", "qty", "price"],
"additionalProperties": false
}
},
"email": { "type": "string", "format": "email" },
"address": { "type": "string" }
},
"required": ["type","userId","items"],
"if": { "properties": { "type": { "const": "virtual" } } },
"then": { "required": ["email"] },
"else": { "required": ["address"] },
"additionalProperties": false
}
""";
@PostMapping
public ResponseEntity<?> create(@RequestBody JsonNode body) {
JsonSchema schema = factory.getSchema(ORDER_CREATE_SCHEMA);
Set<ValidationMessage> errors = schema.validate(body);
if (!errors.isEmpty()) {
throw new SchemaValidationException(errors);
}
// 通过校验后再转换为业务 DTO(可选)
CreateOrderRequest req = mapper.convertValue(body, CreateOrderRequest.class);
// 业务处理...
return ResponseEntity.ok(Map.of("ok", true));
}
}
全局异常处理
@ResponseStatus(HttpStatus.BAD_REQUEST)
class SchemaValidationException extends RuntimeException {
private final Set<ValidationMessage> errors;
SchemaValidationException(Set<ValidationMessage> errors) { this.errors = errors; }
public Set<ValidationMessage> getErrors() { return errors; }
}
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(SchemaValidationException.class)
public ResponseEntity<Map<String, Object>> onSchemaError(SchemaValidationException ex) {
List<Map<String, Object>> details = ex.getErrors().stream().map(e -> Map.of(
"path", e.getPath(), // $.items[0].qty
"message", e.getMessage() // must be greater than or equal to 1
)).toList();
return ResponseEntity.badRequest().body(Map.of(
"code", "INVALID_ARGUMENT",
"errors", details
));
}
}
3.2 资源文件/远程加载(团队协作友好)
把 .schema.json
放到 resources/schemas/
,按业务对象 + 版本命名,例如:
resources/schemas/order/create/v1.schema.json
resources/schemas/order/create/v2.schema.json
加载方式:
@Component
public class ClasspathSchemaLoader implements SchemaLoader {
private final JsonSchemaFactory factory =
JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
@Override
public JsonSchema load(String path) {
try (InputStream in = getClass().getResourceAsStream("/schemas/" + path)) {
if (in == null) throw new IllegalArgumentException("Schema not found: " + path);
return factory.getSchema(in);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
interface SchemaLoader {
JsonSchema load(String path);
}
Controller 中按“业务名 + 版本”决定加载哪份:
@PostMapping("/v2")
public ResponseEntity<?> createV2(@RequestBody JsonNode body) {
JsonSchema schema = schemaLoader.load("order/create/v2.schema.json");
Set<ValidationMessage> errors = schema.validate(body);
if (!errors.isEmpty()) throw new SchemaValidationException(errors);
// ...
return ResponseEntity.ok().build();
}
3.3 数据库存储 + 动态热加载(高扩展)
适合:规则经常变化、希望“配置改了立即生效”。
表结构(示意,存字符串最通用)
CREATE TABLE schema_def (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(128) NOT NULL, -- 业务名,如 order.create
version VARCHAR(32) NOT NULL, -- 版本,如 v1、2025-08-01
schema_text TEXT NOT NULL, -- JSON Schema 字符串
status VARCHAR(16) NOT NULL DEFAULT 'active', -- active/archived
created_at TIMESTAMP NOT NULL DEFAULT now(),
UNIQUE (name, version)
);
JPA 实体与仓库(简化)
@Entity @Table(name = "schema_def")
class SchemaDef {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id;
String name;
String version;
@Lob String schemaText;
String status;
Timestamp createdAt;
// getters/setters...
}
public interface SchemaDefRepo extends JpaRepository<SchemaDef, Long> {
Optional<SchemaDef> findFirstByNameAndVersionAndStatus(String name, String version, String status);
}
动态加载 + 本地缓存
@Component
public class DbBackedSchemaRegistry {
private final SchemaDefRepo repo;
private final JsonSchemaFactory factory =
JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
private final ConcurrentMap<String, JsonSchema> cache = new ConcurrentHashMap<>();
public DbBackedSchemaRegistry(SchemaDefRepo repo) { this.repo = repo; }
public JsonSchema get(String name, String version) {
String key = name + ":" + version;
return cache.computeIfAbsent(key, k -> {
SchemaDef def = repo.findFirstByNameAndVersionAndStatus(name, version, "active")
.orElseThrow(() -> new IllegalArgumentException("Schema not found: " + key));
return factory.getSchema(def.getSchemaText());
});
}
public void evict(String name, String version) {
cache.remove(name + ":" + version);
}
}
使用
@PostMapping("/dynamic/{version}")
public ResponseEntity<?> createDynamic(@PathVariable String version, @RequestBody JsonNode body) {
JsonSchema schema = registry.get("order.create", version);
Set<ValidationMessage> errors = schema.validate(body);
if (!errors.isEmpty()) throw new SchemaValidationException(errors);
return ResponseEntity.ok().build();
}
运维上可配一个“刷新缓存”管理接口,在你更新数据库规则后调用
evict()
或全量清理。
4. 可复用的“Schema 注册中心”与缓存设计
目标:统一入口 + 预编译缓存 + 容错。
- Key 设计:
{domain}.{action}:{version}
,例如order.create:v2
。 - 缓存:
ConcurrentHashMap
简单可用;生产建议 Caffeine(支持 TTL/最大容量/统计)。 - 预热:应用启动时预编译“热点 Schema”。
- 容错:加载失败/Schema 无效时快速告警(日志 + 指标 + 健康检查失败)。
- 并发:
computeIfAbsent
避免同一 key 的并发重复编译。
5. 控制器入参校验的落地:拦截、报错、返回体规范
拦截层级选择:
- Controller 层(上文示例):最直观,便于因接口不同选择不同 Schema。
- Filter/HandlerInterceptor:适合统一入口(如按路由选择 Schema),但需要解析请求体两次(注意缓存流)。
- 自定义
@SchemaValidated(name="order.create", version="v2")
:结合 AOP 实现“所见即所得”的注解式体验。
错误返回体建议统一:
{
"code": "INVALID_ARGUMENT",
"errors": [
{ "path": "$.items[0].qty", "message": "must be >= 1" },
{ "path": "$.email", "message": "must be a valid email" }
],
"requestId": "..." // 便于追踪
}
6. 复杂场景示例
6.1 条件约束:虚拟商品需要邮箱,实物需要地址
(已在 3.1 示例展示)
6.2 互斥/择一(oneOf
)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"wechatId": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" }
},
"oneOf": [
{ "required": ["wechatId"] },
{ "required": ["email"] }
],
"additionalProperties": false
}
6.3 数组约束 + 去重
{
"type": "array",
"items": { "type": "string", "minLength": 1 },
"minItems": 1,
"uniqueItems": true
}
6.4 复用与拆分:$ref
+ 组件化
common/address.schema.json
{
"$id": "https://example.com/schemas/common/address.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"province": { "type": "string" },
"city": { "type": "string" },
"street": { "type": "string" },
"zip": { "type": "string", "pattern": "^[0-9]{6}$" }
},
"required": ["province","city","street","zip"],
"additionalProperties": false
}
order/create/v2.schema.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://example.com/schemas/order/create/v2.schema.json",
"type": "object",
"properties": {
"type": { "enum": ["physical","virtual"] },
"userId": { "type": "string" },
"items": { "$ref": "items.schema.json" },
"address": { "$ref": "https://example.com/schemas/common/address.schema.json" }
},
"required": ["type","userId","items"],
"if": { "properties": { "type": { "const": "physical" } } },
"then": { "required": ["address"] },
"additionalProperties": false
}
注意:跨文件
$ref
需要你的加载器支持解析相对/绝对$id
与引用。networknt
支持通过SchemaValidatorsConfig
等配置控制解析策略;你也可以实现自定义URIFetcher
(高级用法)。
7. 与 @Valid
的协同:分层职责与迁移策略
推荐分工:
- JSON Schema:负责边界格式与结构校验(字段类型、范围、必填、条件组合)。
@Valid
/业务校验:负责领域规则(与数据库/外部系统相关,如 SKU 是否存在、库存是否充足、状态流转是否合法)。
迁移策略:
- 先把变动频繁、跨语言的接口迁到 JSON Schema。
- 保留
@Valid
处理稳定的 DTO 与领域规则。 - 新接口默认先上 Schema;内部服务间、性能敏感的可保留
@Valid
。
8. 性能与稳定性建议
- 预编译 Schema:
factory.getSchema()
相对昂贵,务必缓存。 - 只在边界校验一次:Controller 校验通过后,内部不要重复。
- 限流与体积限制:防止超大 JSON DoS;Servlet 层/网关层限制 Body 大小。
- 数字精度:金额用
string
+ 业务侧BigDecimal
解析,避免浮点误差。 date-time
语义:Schema 只校验格式,不校验时区/业务区间;时间区间请在业务层做。- 错误可读性:必要时自定义“友好提示”映射(英文转中文、路径压缩)。
- 观测:统计“校验失败率/Top 错误字段/平均耗时”,问题可回溯。
9. 运维与治理:版本化、灰度、回滚
- 版本号策略:
v1
、v2
或日期戳2025-08-01
;同一时间只维护一个 active。 - 灰度:按 Header 或租户路由到不同版本 Schema(网关/Controller 层实现)。
- 回滚:保留旧版本 Schema,切换路由即可秒回滚。
- 变更审计:Schema 变更走评审流程(PR/DB 审批),保留修改人/理由。
- 兼容性:新增字段默认非必填;删除字段先标记弃用,再移除。
10. 附录:完整代码片段汇总
10.1 统一的校验服务
@Component
public class JsonSchemaValidatorService {
private final ObjectMapper mapper = new ObjectMapper();
public void validate(JsonSchema schema, JsonNode body) {
Set<ValidationMessage> errors = schema.validate(body);
if (!errors.isEmpty()) throw new SchemaValidationException(errors);
}
public void validate(JsonSchema schema, String json) {
try {
validate(schema, mapper.readTree(json));
} catch (IOException e) {
throw new SchemaValidationException(Set.of(new ValidationMessage.Builder()
.path("$").format("invalid JSON").build()));
}
}
}
10.2 AOP 注解式(可选)
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SchemaValidated {
String name();
String version();
}
@Aspect
@Component
class SchemaValidationAspect {
private final DbBackedSchemaRegistry registry;
private final JsonSchemaValidatorService service;
private final ObjectMapper mapper = new ObjectMapper();
@Around("@annotation(ann)")
public Object around(ProceedingJoinPoint pjp, SchemaValidated ann) throws Throwable {
Object[] args = pjp.getArgs();
// 假设第一个 @RequestBody 是 JSON
for (Object arg : args) {
if (arg instanceof JsonNode node) {
JsonSchema schema = registry.get(ann.name(), ann.version());
service.validate(schema, node);
break;
}
}
return pjp.proceed();
}
}
10.3 单元测试(示意)
@SpringBootTest
@AutoConfigureMockMvc
class OrderApiTest {
@Autowired MockMvc mvc;
@Test
void create_virtual_order_missing_email_should_fail() throws Exception {
String json = """
{"type":"virtual","userId":"u1","items":[{"sku":"A","qty":1,"price":9.9}]}
""";
mvc.perform(post("/orders").contentType(MediaType.APPLICATION_JSON).content(json))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors[0].path").exists());
}
}
结语
- 如果你的接口只在 Java 内部使用、规则稳定,
@Valid
已足够。 - 一旦进入跨语言协作、快速变更、复杂结构/条件的领域,JSON Schema 能把“规则”抽离成资产,实现前后端/多语言统一与灵活演进。
- 按本文给出的三种集成方式,先从资源文件加载 + 缓存起步,逐步演进到数据库动态热加载 + 版本治理,即可在生产环境稳妥落地。