Skip to content

feat: add ValidationHandler to JSONSchema for fail-safe error collection, for issue #3912#3968

Open
jujn wants to merge 5 commits intoalibaba:mainfrom
jujn:feat_3912
Open

feat: add ValidationHandler to JSONSchema for fail-safe error collection, for issue #3912#3968
jujn wants to merge 5 commits intoalibaba:mainfrom
jujn:feat_3912

Conversation

@jujn
Copy link
Copy Markdown
Collaborator

@jujn jujn commented Jan 28, 2026

What this PR does / why we need it?

在 JSONSchema 加入 ValidationHandler 以支持全量错误收集(已支持全量收集自定义错误信息),通过回调机制允许用户控制校验流程(中断/继续)。

代码示例:

使用示例①
    public void test() {
        // 1. 定义 Schema (校验规则)
        String schemaStr = "{\n" +
                "  \"type\": \"object\",\n" +
                "  \"properties\": {\n" +
                "    \"username\": { \"type\": \"string\", \"minLength\": 5 },\n" +
                "    \"age\": { \"type\": \"integer\", \"minimum\": 18 },\n" +
                "    \"email\": { \"type\": \"string\", \"format\": \"email\" }\n" +
                "  },\n" +
                "  \"required\": [\"username\", \"email\"]\n" +
                "}";

        JSONSchema schema = JSONSchema.parseSchema(schemaStr);

        // 2. 构造一个包含 3 个错误的 数据
        JSONObject badData = JSONObject.of(
                "username", "cat",         // 错误1: 长度小于 5
                "age", "too young",        // 错误2: 类型错误 (String 而不是 Integer)
                "email", "not-an-email"    // 错误3: 格式不对
        );

        // 3. 准备一个容器来装错误
        List<String> errorList = new ArrayList<>();

        // 4. 定义 Handler
        JSONSchema.ValidationHandler handler = (parentSchema, value, message, path) -> {
            String log = String.format("字段 [%s] 校验失败: %s (当前值: %s)", path, message, value);
            errorList.add(log);
            return true; // 返回 true 继续校验,false 则停止
        };

        // 5. 执行校验
        schema.validate(badData, handler);

        // 6. 输出结果
        for (String err : errorList) {
            System.out.println(err);
        }
    }
使用示例②(Web请求校验)
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
public class Issue3912 {
    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @BeforeEach
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
                .addFilter(new CharacterEncodingFilter("UTF-8", true))
                .build();
    }

    /**
     * 测试场景 : 数据包含多个错误,应该返回 400 Bad Request,并包含所有错误信息
     * 错误 1: username 长度不够 (min 5)
     * 错误 2: age 未满 18 (min 18)
     */
    @Test
    public void testValidationFail() throws Exception {
        String requestJson = "{\"username\":\"a\",\"age\":10}";
        mockMvc.perform(
                        (post("/issue3912")
                                .characterEncoding("UTF-8")
                                .contentType(MediaType.APPLICATION_JSON_VALUE)
                                .content(requestJson)
                        ))
                .andExpect(status().isBadRequest())
                .andExpect(content().string(containsString("用户名长度不能小于5")))
                .andExpect(content().string(containsString("必须年满18岁")));
    }

    @RestController
    public static class TestController {
        @PostMapping(value = "/issue3912", produces = "application/json")
        public String issue3912(@RequestBody UserBean user) {
            return "success";
        }
    }

    @Data
    public static class UserBean {
        private String username;
        private Integer age;
    }

    /**
     * 自定义异常,用于携带校验错误信息
     */
    public static class SchemaValidationException extends RuntimeException {
        private final List<String> errors;

        public SchemaValidationException(List<String> errors) {
            this.errors = errors;
        }

        public List<String> getErrors() {
            return errors;
        }
    }

    /**
     * 在 JSON 反序列化成对象后,Controller 执行前,进行 Schema 校验
     */
    @RestControllerAdvice
    public static class SchemaValidationAdvice extends RequestBodyAdviceAdapter {
        private static final JSONSchema USER_SCHEMA = JSONSchema.parseSchema("{\n" +
                "  \"type\": \"object\",\n" +
                "  \"properties\": {\n" +
                "    \"username\": {\n" +
                "      \"type\": \"string\",\n" +
                "      \"minLength\": 5,\n" +
                "      \"error\": \"用户名长度不能小于5\"\n" + // 自定义错误消息
                "    },\n" +
                "    \"age\": {\n" +
                "      \"type\": \"integer\",\n" +
                "      \"minimum\": 18,\n" +
                "      \"error\": \"必须年满18岁\"\n" + // 自定义错误消息
                "    }\n" +
                "  }\n" +
                "}");

        @Override
        public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
            return targetType == UserBean.class; // 仅拦截 UserBean
        }

        @Override
        public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
            if (body instanceof UserBean) {
                List<String> errors = new ArrayList<>();

                USER_SCHEMA.validate(body, (schema, value, message, path) -> {
                    errors.add(path + ": " + message);
                    return true;
                });

                if (!errors.isEmpty()) {
                    throw new SchemaValidationException(errors);
                }
            }
            return body;
        }
    }

    @RestControllerAdvice
    public static class GlobalExceptionHandler {
        @ExceptionHandler(SchemaValidationException.class)
        public ResponseEntity<Map<String, Object>> handleException(SchemaValidationException ex) {
            Map<String, Object> body = new HashMap<>();
            body.put("errors", ex.getErrors());
            return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
        }
    }

    @ComponentScan(basePackages = "com.alibaba.fastjson2.spring.issues.issue3912")
    @Configuration
    @Order(Ordered.LOWEST_PRECEDENCE + 1)
    @EnableWebMvc
    public static class WebMvcConfig implements WebMvcConfigurer {
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
            FastJsonConfig fastJsonConfig = new FastJsonConfig();
            fastConverter.setFastJsonConfig(fastJsonConfig);
            fastConverter.setDefaultCharset(StandardCharsets.UTF_8);
            List<MediaType> supportedMediaTypes = new ArrayList<>();
            supportedMediaTypes.add(MediaType.APPLICATION_JSON);
            fastConverter.setSupportedMediaTypes(supportedMediaTypes);
            converters.add(0, fastConverter);
        }
    }
}

Summary of your change

  1. 接口定义 (ValidationHandler):
  • 定义 handle(schema, value, message, path) 方法。
  • 返回 true 表示“已处理,可继续校验后续节点”,用户可以在回调中收集 message 错误信息;返回 false 表示“立即中断”(同 schema 校验默认逻辑)。
  1. 基类改动 (JSONSchema):
  • 新增 validate(value, handler) 入口方法。
  • 新增 handleError 辅助方法,封装“应用 customErrorMessage -> 调用 handler -> 处理中断”通用逻辑。
  1. 叶节点改动 (StringSchema、IntegerSchema……):
  • 当发现错误时,不再直接 return,而是调用 handleError。
  1. 容器节点改动 (ObjectSchema,、ArraySchema):
  • 当收到子节点返回的失败结果时,检查 handler 和 result.isAbort() 。
  • Fail-Fast:如果 isAbort() 为 true,立即向上层 return,中断整个校验树的遍历。
  • Fail-Safe:如果 isAbort() 为 false,说明用户希望继续。容器节点会记录当前失败状态(totalSuccess = false),然后继续循环去校验下一个节点,从而收集所有错误。
  1. 组合逻辑改动 (AnyOf、OneOf、AllOf 等):
  • 逻辑同容器、叶子节点。
  1. 具体改动逻辑**(供 review 参考)**:
  • 改动针对所有返回 ValidateResult 的地方
  • 如果原代码返回 new ValidateResult 或静态常量 ValidateResult,则 return handleError(…) (容器节点中需要判断 handler==null 和 result.isAbort,决定中断/继续)
  • 如果原代码返回 子scheme.validateInternal()得到的result,只需判断 handler==null 和 result.isAbort,决定中断/继续

@safe1ine
Copy link
Copy Markdown

safe1ine commented Feb 6, 2026

@MonkeyCode-AI review 一下

@MonkeyCode-AI
Copy link
Copy Markdown

MonkeyCode-AI 正在分析任务...

Copy link
Copy Markdown

@MonkeyCode-AI MonkeyCode-AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我是 MonkeyCode AI 编程助手,你可以在 GitHub 仓库的 PR 中 at @MonkeyCode-AI 来呼唤我。

任务执行细节请参考: https://monkeycode-ai.com/tasks/public?id=51e2b2b6-be8e-42e2-82bd-298457dcc212

代码审查结果

本 PR 通过引入 ValidationHandler 让 Fail-Safe/Fail-Fast 并存且测试较完善,但仍存在组合校验绕过 handler、错误封装不一致与可诊断性不足等问题,建议修复后再合入。

✨ 代码亮点

  • 通过 ValidationHandler + abort 标志实现 Fail-Safe / Fail-Fast 双模式,API 简洁直观
  • JSONSchema.handleError 将 custom error message 解析与 handler 回调集中封装,降低各 Schema 重复逻辑
  • 新增 Issue3912 单元测试覆盖了收集全部错误、提前中断、嵌套对象/数组、dependentRequired、additionalProperties、uniqueItems 等关键场景
🚨 Critical ⚠️ Warning 💡 Suggestion
1 0 0

Comment thread core/src/main/java/com/alibaba/fastjson2/schema/StringSchema.java
Comment thread core/src/main/java/com/alibaba/fastjson2/schema/ObjectSchema.java Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants