在 Spring Boot 中使用 JSON Schem 實現復雜對象、可變結構參數校驗!
1. 為什么要在 Spring Boot 中使用 JSON Schem進行校驗
1.1 傳統校驗方式的局限
? 手寫 POJO + @Valid:只能校驗映射到 Java 對象的字段,面對 深層嵌套、可變結構(如數組中對象字段不統一)時往往需要大量的自定義 DTO 與轉換代碼。
? 手動解析 + if 判斷:代碼冗余、易遺漏、錯誤信息不統一,維護成本隨業務增長呈指數級上升。
1.2 JSON Schema 的優勢
優勢 | 說明 |
聲明式 | 通過 JSON 文檔描述結構、約束、默認值,業務代碼無需關心細節。 |
可組合 | 支持 |
跨語言 | 同一套 Schema 可在前端、后端、測試腳本等多端共享,保證全鏈路一致性。 |
錯誤定位精準 | 校驗器會返回錯誤路徑(JSON Pointer),便于快速定位問題字段。 |
可擴展 | 支持自定義關鍵字,滿足業務特有的校驗需求(如唯一性、業務規則校驗)。 |
在微服務架構中,接口契約往往以 JSON Schema 形式保存于 API 文檔中心(如 Swagger、OpenAPI),將其直接用于運行時校驗是最自然的落地方式。
2. JSON Schema 基礎概念與核心關鍵字
2.1 基本結構
{
"$schema":"http://json-schema.org/draft-07/schema#",
"title":"用戶信息",
"type":"object",
"properties":{
"id": {"type":"integer","minimum":1},
"name":{"type":"string","minLength":1},
"email":{"type":"string","format":"email"},
"roles":{
"type":"array",
"items":{"type":"string","enum":["ADMIN","USER","GUEST"]},
"minItems":1,
"uniqueItems":true
}
},
"required":["id","name","email"]
}? type:限定數據類型(object、array、string、number、boolean、null)。
? properties:對象屬性的子 Schema。
? required:必須出現的屬性列表。
? enum:枚舉值集合。
? format:預定義格式(email、date-time、uri 等)。
2.2 常用關鍵字速查
關鍵字 | 作用 | 示例 |
/ | 數值范圍 |
|
/ | 開區間 |
|
/ | 字符串長度 |
|
| 正則匹配 |
|
/ | 數組元素個數 |
|
| 數組元素唯一性 |
|
| 是否允許未聲明屬性 |
|
| 屬性間依賴 |
|
/ | 組合約束 |
|
| 引用外部或內部 Schema |
|
| 默認值(僅在生成時有意義) |
|
2.3 版本兼容性
? Draft-07 是目前最廣泛支持的版本,幾乎所有 Java 校驗庫均兼容。
? 若項目需要 2020?12 或 2023?09 的新特性(如 unevaluatedProperties),需要確認所選庫已實現對應草案。
3. Spring Boot 項目準備與依賴選型
3.1 項目結構(示例)
src/main/java
└─ com.example.demo
├─ controller
├─ service
├─ validator // 自定義校驗器
└─ config // Spring 配置
src/main/resources
└─ schemas
└─ user-schema.json3.2 主流 JSON Schema 校驗庫對比
庫 | Maven 坐標 | 主要特性 | 備注 |
NetworkNT json-schema-validator |
| 完全實現 Draft?07、支持 | 社區活躍,文檔完整 |
Everit JSON Schema |
| 輕量、異常信息友好、支持 Draft?07 | 依賴 |
Justify |
| 支持 Draft?07、流式校驗、低內存占用 | 適合大文件校驗 |
Jackson-module-jsonSchema |
| 與 Jackson 緊耦合、生成 Schema 為主 | 生成能力強,校驗功能相對弱 |
推薦:在 Spring Boot 項目中使用 NetworkNT,因為它提供了 JsonSchemaFactory、Validator、ValidationMessage 等易于集成的 API,并且對 $ref 的解析支持良好。
3.3 Maven 依賴示例
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JSON Schema Validator (NetworkNT) -->
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>1.0.86</version>
</dependency>
<!-- Jackson (已隨 Spring Boot 引入) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Lombok(可選) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>注意:json-schema-validator 依賴 jackson-databind 進行 JSON 讀取,確保版本兼容。
4. 基于 json-schema-validator(NetworkNT)實現自動校驗
4.1 加載 Schema
package com.example.demo.config;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.InputStream;
@Configuration
publicclassJsonSchemaConfig {
privatefinalObjectMappermapper=newObjectMapper();
/** 讀取 classpath 下的 schema 文件并返回 JsonSchema 實例 */
@Bean
public JsonSchema userSchema()throws Exception {
try (InputStreamis= getClass().getResourceAsStream("/schemas/user-schema.json")) {
JsonNodeschemaNode= mapper.readTree(is);
JsonSchemaFactoryfactory= JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
.objectMapper(mapper)
.build();
return factory.getSchema(schemaNode);
}
}
}? SpecVersion.VersionFlag.V7 指定使用 Draft?07。
? 通過 @Bean 將 JsonSchema 注入 Spring 容器,后續可直接 @Autowired 使用。
4.2 編寫校驗工具類
package com.example.demo.validator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.ValidationMessage;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
publicclassJsonValidator {
privatefinalObjectMappermapper=newObjectMapper();
/** 校驗 JSON 字符串是否符合給定的 Schema,返回錯誤集合 */
public Set<ValidationMessage> validate(String json, JsonSchema schema)throws Exception {
JsonNodenode= mapper.readTree(json);
return schema.validate(node);
}
/** 將錯誤集合轉為統一的錯誤信息字符串(可自行改造為錯誤對象) */
public String formatErrors(Set<ValidationMessage> errors) {
StringBuildersb=newStringBuilder();
for (ValidationMessage msg : errors) {
sb.append("路徑 ").append(msg.getPath())
.append(" : ").append(msg.getMessage())
.append("; ");
}
return sb.toString();
}
}? ValidationMessage#getPath() 返回 JSON Pointer(如 /roles/0),幫助前端定位。
? validate 方法拋出異常僅用于 JSON 解析錯誤,業務校驗錯誤通過返回集合處理。
4.3 在 Controller 中使用
package com.example.demo.controller;
import com.example.demo.validator.JsonValidator;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.ValidationMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Set;
@RestController
@RequestMapping("/api/users")
publicclassUserController {
@Autowired
private JsonSchema userSchema; // 注入的 Schema Bean
@Autowired
private JsonValidator validator; // 校驗工具
@PostMapping
public ResponseEntity<?> createUser(@RequestBody String rawJson) {
try {
Set<ValidationMessage> errors = validator.validate(rawJson, userSchema);
if (!errors.isEmpty()) {
// 返回 400 并附帶錯誤詳情
return ResponseEntity.badRequest()
.body(validator.formatErrors(errors));
}
// 業務處理:將 JSON 反序列化為 POJO、持久化等
// User user = objectMapper.readValue(rawJson, User.class);
// userService.save(user);
return ResponseEntity.ok("校驗通過,業務處理完成");
} catch (Exception e) {
// JSON 解析異常或其他內部錯誤
return ResponseEntity.status(500).body("服務器內部錯誤:" + e.getMessage());
}
}
}? 核心思路:把原始請求體保留為字符串,先交給校驗器;只有在校驗通過后才進行業務層的對象映射與持久化。
? 這樣可以 避免因反序列化錯誤導致的異常泄露,并且錯誤信息直接對應 JSON Schema 定義。
5. 自定義關鍵字與擴展校驗邏輯
5.1 業務場景示例
假設業務要求 用戶名在同一租戶內唯一,這屬于跨記錄的業務規則,JSON Schema 本身不提供此類校驗。我們可以通過 自定義關鍵字 uniqueInTenant 來實現。
5.2 實現自定義關鍵字
package com.example.demo.config;
import com.networknt.schema.*;
import com.networknt.schema.keyword.Keyword;
import com.networknt.schema.keyword.KeywordFactory;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Set;
/** 自定義關鍵字工廠 */
@Component
publicclassCustomKeywordFactoryimplementsKeywordFactory {
@Override
public Set<String> getKeywords() {
return Collections.singleton("uniqueInTenant");
}
@Override
public Keyword createKeyword(String keyword, JsonNode node) {
returnnewUniqueInTenantKeyword(node);
}
/** 關鍵字實現類 */
staticclassUniqueInTenantKeywordimplementsKeyword {
privatefinal JsonNode schemaNode;
UniqueInTenantKeyword(JsonNode schemaNode) {
this.schemaNode = schemaNode;
}
@Override
public ValidationResult validate(JsonNode node, JsonNode rootNode, String at) {
// 這里的 node 為待校驗的字段值(如 username)
Stringusername= node.asText();
// 假設有一個租戶 ID 已經在上下文中獲取
StringtenantId= ValidationContext.getCurrentTenantId();
// 調用業務服務檢查唯一性(這里用偽代碼演示)
booleanexists= UserService.isUsernameExistsInTenant(username, tenantId);
if (exists) {
return ValidationResult.error(at, "用戶名在當前租戶內已存在");
}
return ValidationResult.ok();
}
@Override
public String getKeyword() {
return"uniqueInTenant";
}
}
}5.3 將自定義關鍵字注冊到 Factory
@Bean
public JsonSchemaFactory jsonSchemaFactory(ObjectMapper mapper, CustomKeywordFactory customFactory) {
return JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
.objectMapper(mapper)
.addKeyword(customFactory) // 注冊自定義關鍵字
.build();
}5.4 在 Schema 中使用
{
"$schema":"http://json-schema.org/draft-07/schema#",
"title":"用戶注冊",
"type":"object",
"properties":{
"username":{
"type":"string",
"minLength":3,
"uniqueInTenant":true // 自定義關鍵字
},
"password":{
"type":"string",
"minLength":8
}
},
"required":["username","password"]
}注意:自定義關鍵字的實現必須是 無副作用 的純函數式校驗,否則可能導致并發安全問題。
6. 全局異常處理與錯誤信息統一返回
6.1 統一錯誤響應結構
{
"timestamp":"2025-10-11T14:23:45.123+08:00",
"status":400,
"error":"Bad Request",
"message":"請求參數校驗失敗",
"details":[
{"path":"/email","msg":"必須是合法的 email 地址"},
{"path":"/roles/0","msg":"不允許的枚舉值"}
]
}6.2 實現 @ControllerAdvice
package com.example.demo.exception;
import com.networknt.schema.ValidationMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.stream.Collectors;
@RestControllerAdvice
publicclassGlobalExceptionHandler {
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidation(ValidationException ex) {
List<FieldError> fieldErrors = ex.getErrors().stream()
.map(v -> newFieldError(v.getPath(), v.getMessage()))
.collect(Collectors.toList());
ErrorResponseresp=newErrorResponse(
ZonedDateTime.now(),
HttpStatus.BAD_REQUEST.value(),
"Bad Request",
"請求參數校驗失敗",
fieldErrors
);
returnnewResponseEntity<>(resp, HttpStatus.BAD_REQUEST);
}
// 其它異常統一處理...
}
/** 自定義異常包裝 ValidationMessage 集合 */
classValidationExceptionextendsRuntimeException {
privatefinal Set<ValidationMessage> errors;
publicValidationException(Set<ValidationMessage> errors) {
this.errors = errors;
}
public Set<ValidationMessage> getErrors() { return errors; }
}
/** 錯誤響應 DTO */
recordErrorResponse(
ZonedDateTime timestamp,
int status,
String error,
String message,
List<FieldError> details) {}
recordFieldError(String path, String msg) {}6.3 在業務層拋出統一異常
Set<ValidationMessage> errors = validator.validate(rawJson, userSchema);
if (!errors.isEmpty()) {
throw new ValidationException(errors);
}這樣,所有校驗錯誤都會統一走 GlobalExceptionHandler,前端只需要解析一次統一結構即可。
7. 在 Controller 中使用校驗注解的完整示例
7.1 定義自定義注解
package com.example.demo.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public@interface JsonValidated {
/** 指定使用的 Schema Bean 名稱 */
@AliasFor("value")
String schema()default"";
@AliasFor("schema")
String value()default"";
}7.2 實現參數解析攔截器
package com.example.demo.resolver;
import com.example.demo.annotation.JsonValidated;
import com.example.demo.exception.ValidationException;
import com.example.demo.validator.JsonValidator;
import com.networknt.schema.JsonSchema;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.*;
import org.springframework.web.method.support.*;
import java.util.Set;
@Component
publicclassJsonValidatedArgumentResolverimplementsHandlerMethodArgumentResolver {
@Autowired
private JsonValidator validator;
@Autowired
private ApplicationContext ctx; // 用于獲取 Schema Bean
@Override
publicbooleansupportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(JsonValidated.class)
&& parameter.getParameterType().equals(String.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory)throws Exception {
JsonValidatedann= parameter.getParameterAnnotation(JsonValidated.class);
StringschemaBeanName= ann.schema();
JsonSchemaschema= (JsonSchema) ctx.getBean(schemaBeanName);
Stringbody= webRequest.getNativeRequest(HttpServletRequest.class)
.getReader()
.lines()
.reduce("", (acc, line) -> acc + line);
Set<ValidationMessage> errors = validator.validate(body, schema);
if (!errors.isEmpty()) {
thrownewValidationException(errors);
}
return body; // 校驗通過后返回原始 JSON 字符串
}
}7.3 注冊解析器
@Configuration
publicclassWebConfigimplementsWebMvcConfigurer {
@Autowired
private JsonValidatedArgumentResolver jsonResolver;
@Override
publicvoidaddArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(jsonResolver);
}
}7.4 使用示例
@PostMapping("/register")
public ResponseEntity<?> register(@JsonValidated("userSchema") String userJson) {
// 此時 userJson 已經通過 userSchema 校驗
// 直接反序列化業務對象
UserDto dto = objectMapper.readValue(userJson, UserDto.class);
userService.register(dto);
return ResponseEntity.ok("注冊成功");
}通過自定義注解 + 參數解析器,校驗邏輯與業務代碼徹底解耦,控制器只關心業務本身。
8. 單元測試與集成測試最佳實踐
8.1 單元測試校驗工具
@SpringBootTest
classJsonValidatorTest {
@Autowired
private JsonValidator validator;
@Autowired
@Qualifier("userSchema")
private JsonSchema schema;
@Test
voidvalidJsonShouldPass()throws Exception {
Stringjson="""
{
"id": 10,
"name": "張三",
"email": "zhangsan@example.com",
"roles": ["ADMIN"]
}
""";
Set<ValidationMessage> errors = validator.validate(json, schema);
assertTrue(errors.isEmpty());
}
@Test
voidinvalidJsonShouldReturnErrors()throws Exception {
Stringjson="""
{
"id": -1,
"name": "",
"email": "not-an-email",
"roles": []
}
""";
Set<ValidationMessage> errors = validator.validate(json, schema);
assertFalse(errors.isEmpty());
// 斷言具體錯誤路徑
assertTrue(errors.stream().anyMatch(v -> v.getPath().equals("$.id")));
assertTrue(errors.stream().anyMatch(v -> v.getPath().equals("$.email")));
}
}8.2 集成測試 Controller
@AutoConfigureMockMvc
@SpringBootTest
classUserControllerTest {
@Autowired
private MockMvc mvc;
@Test
voidcreateUserWhenInvalidShouldReturn400()throws Exception {
Stringpayload="""
{
"id": 0,
"name": "",
"email": "bad",
"roles": ["UNKNOWN"]
}
""";
mvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.details[?(@.path=='/email')].msg")
.value("must be a valid email address"));
}
}? 使用 MockMvc 可以完整走一遍 請求 → 校驗 → 異常處理 → 響應 流程,確保錯誤信息的結構與內容符合約定。
9. 性能考量與緩存策略
9.1 Schema 加載成本
? 一次性加載:在啟動階段把所有 Schema 讀取為 JsonSchema 對象并放入 Spring 容器(如前文 @Bean),后續直接復用,避免每次請求重新解析。
? 緩存 JsonSchema:JsonSchemaFactory 本身提供內部緩存(基于 $id),但顯式緩存可以更好控制生命周期。
9.2 校驗過程的 CPU 與內存占用
? CPU:校驗本質是遍歷 JSON 樹并匹配關鍵字,復雜度與 JSON 大小呈線性關系。對大文件(>10?MB)建議使用 流式校驗(Justify)或分塊校驗。
? 內存:ObjectMapper.readTree 會把整個 JSON 讀取為樹結構,若業務只需要校驗而不需要后續對象映射,可在校驗后直接丟棄樹對象,減輕 GC 壓力。
9.3 并發場景下的優化
場景 | 優化手段 |
高并發短請求 | 采用 線程安全的 |
大批量數據校驗 | 使用 批量緩存:一次性校驗一個數組,返回每條記錄的錯誤集合,減少線程切換。 |
多租戶環境 | 將租戶相關的自定義關鍵字實現 無狀態,通過 |
10. 進階特性:條件校驗、動態模式、格式化校驗
10.1 條件校驗(if/then/else)
{
"type":"object",
"properties":{
"type":{"enum":["PERSON","COMPANY"]},
"personInfo":{"$ref":"#/definitions/person"},
"companyInfo":{"$ref":"#/definitions/company"}
},
"required":["type"],
"if":{
"properties":{"type":{"const":"PERSON"}}
},
"then":{
"required":["personInfo"]
},
"else":{
"required":["companyInfo"]
},
"definitions":{
"person":{"type":"object","properties":{"name":{"type":"string"}}},
"company":{"type":"object","properties":{"name":{"type":"string"}}}
}
}? if/then/else 讓同一對象在不同業務分支下擁有不同必填字段,極大提升 單一 Schema 的表達能力。
10.2 動態模式(patternProperties)
{
"type":"object",
"patternProperties":{
"^prop_[0-9]+$":{"type":"string"}
},
"additionalProperties":false
}? 適用于 鍵名可變、但鍵值類型統一的場景(如動態屬性表、標簽集合)。
10.3 自定義格式(format)
JSON Schema 預定義的 format 包括 email、date-time、uri 等。若業務需要 手機號、身份證號 等自定義格式,可通過 自定義 FormatValidator 注入到 JsonSchemaFactory:
JsonSchemaFactory factory = JsonSchemaFactory.builder()
.formatValidator("phone", value ->
Pattern.matches("^1[3-9]\\d{9}$", value) ? Optional.empty()
: Optional.of("不是合法的手機號"))
.build();隨后在 Schema 中使用:
{
"type": "string",
"format": "phone"
}11. 常見問題排查與調優技巧
問題 | 可能原因 | 解決方案 |
校驗報 | JSON 讀取為 | 在 Controller 首先判斷請求體是否為空;在 Schema 明確 |
錯誤路徑不準確 | 使用了 | 將 |
自定義關鍵字不生效 | 未把自定義 | 確認 |
性能瓶頸在 JSON 解析 | 大文件每次都完整讀取為樹結構 | 改用 流式校驗(Justify)或 分塊讀取(Jackson |
多租戶唯一校驗失效 |
中租戶信息未正確傳遞 | 使用 |
錯誤信息中文亂碼 |
默認使用英文描述 | 在 |
12. 結語:讓 JSON 校驗成為項目的安全防線
? 聲明式 的 JSON Schema 把“數據結構約束”從業務代碼中抽離,使得 接口契約 與 實現 分離,降低了后期變更的風險。
? 統一的校驗入口(如自定義注解 + 參數解析器)讓所有入口點的校驗行為保持一致,避免遺漏。
? 自定義關鍵字 與 格式校驗 能夠把業務規則直接嵌入 Schema,進一步提升系統的防御能力。
? 通過 緩存、流式校驗 與 錯誤信息統一化,我們可以在保持高吞吐的同時,提供友好的錯誤反饋。






























