Quarkus/JAX-RS中如何让JSON-B反序列化报告多个数据绑定错误
问题结论
JSON-B 规范及 Quarkus 默认搭载的 Yasson 实现默认采用 fail-fast 设计,遇到第一个类型转换错误时会直接抛出JsonbException终止反序列化流程,没有提供开箱即用的全量错误收集配置项。可以通过以下两种方案实现全量无效字段错误收集:
方案1:自定义JSON-B反序列化器 + Bean Validation 统一错误收集
这个方案不需要替换现有JSON-B依赖,核心思路是把反序列化阶段的类型错误暂存,不立即抛出,等整个JSON结构解析完成后,通过Bean Validation机制统一输出所有错误。
- 首先定义线程级的错误暂存容器,避免不同请求的错误串扰:
public class DeserializationErrorHolder { private static final ThreadLocal<List<FieldError>> ERRORS = ThreadLocal.withInitial(ArrayList::new); public static void addError(String field, Object invalidValue, String message) { ERRORS.get().add(new FieldError(field, invalidValue, message)); } public static List<FieldError> drainErrors() { List<FieldError> errors = new ArrayList<>(ERRORS.get()); ERRORS.remove(); return errors; } public record FieldError(String field, Object invalidValue, String message) {} }
- 为目标枚举类型编写宽松反序列化器,遇到非法值时记录错误而非直接抛出:
public class LenientChoiceEnumDeserializer implements JsonbDeserializer<Something.Choice> { @Override public Something.Choice deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) { String value = parser.getString(); try { return Something.Choice.valueOf(value); } catch (IllegalArgumentException e) { // 此处可通过解析parser上下文获取当前字段名,例如b、c String currentPath = getCurrentFieldPath(parser); DeserializationErrorHolder.addError(currentPath, value, "枚举值不合法,允许值为X/Y/Z"); return null; // 非法值返回null,继续解析后续字段 } } }
注意:Yasson默认未直接暴露当前解析节点字段名的公共API,可通过反射获取parser内部的当前上下文路径,或针对每个待反序列化字段单独绑定固定字段名,简化实现。
- 注册自定义反序列化器到JSON-B配置,在Quarkus里可以通过CDI Bean提供自定义的
JsonbConfig:
@Singleton public class JsonbConfigProducer { @Produces public JsonbConfig customJsonbConfig() { return new JsonbConfig() .withDeserializers(new LenientChoiceEnumDeserializer()); } }
- 编写类级别的Bean Validation校验器,在反序列化完成后取出所有暂存的错误,统一触发校验失败:
@Target({TYPE}) @Retention(RUNTIME) @Constraint(validatedBy = SomethingValidator.class) public @interface ValidSomething { String message() default "参数校验失败"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } public class SomethingValidator implements ConstraintValidator<ValidSomething, Something> { @Override public boolean isValid(Something value, ConstraintValidatorContext context) { List<DeserializationErrorHolder.FieldError> errors = DeserializationErrorHolder.drainErrors(); if (errors.isEmpty()) { return true; } // 禁用默认错误信息,逐个添加收集到的字段错误 context.disableDefaultConstraintViolation(); for (DeserializationErrorHolder.FieldError error : errors) { context.buildConstraintViolationWithTemplate(error.message()) .addPropertyNode(error.field()) .addConstraintViolation(); } return false; } }
最后在Something类上加上@ValidSomething注解即可,请求到达接口层时,所有字段的反序列化错误会和普通Bean Validation错误一起返回,不会在第一个错误处终止。
方案2:替换序列化框架为Jackson(实现成本更低)
如果没有强依赖JSON-B的专属特性,可以直接切换到Quarkus官方支持的Jackson扩展,Jackson原生支持自定义反序列化错误处理器,不需要自己维护错误暂存逻辑:
- 引入Jackson扩展,移除JSON-B相关依赖:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-jackson</artifactId> </dependency>
- 自定义
DeserializationProblemHandler,遇到反序列化错误时记录后继续解析:
@Singleton public class LenientDeserializationHandler extends DeserializationProblemHandler { @Override public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser p, JsonDeserializer<?> deserializer, Object beanOrClass, String propertyName) throws IOException { // 处理未知字段逻辑,可记录错误后跳过 p.skipChildren(); return true; } @Override public Object handleWeirdStringValue(DeserializationContext ctxt, Class<?> targetType, String valueToConvert, String failureMsg) throws IOException { // 针对枚举等字符串转换错误,记录错误后返回null继续解析 if (targetType.isEnum()) { ctxt.reportInputMismatch(targetType, "字段值%s不合法,允许值为%s", valueToConvert, Arrays.toString(targetType.getEnumConstants())); return null; } return super.handleWeirdStringValue(ctxt, targetType, valueToConvert, failureMsg); } }
- 注册该处理器到Jackson的
ObjectMapper中,配置DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES为false即可实现全量错误收集。
注意事项
- 自定义宽松反序列化逻辑时,要注意嵌套对象、数组字段的路径解析,确保返回的错误字段名和前端传参路径一致
- 暂存错误的ThreadLocal一定要在请求处理完成后清理,避免内存泄漏
- 可以配合Quarkus的
ExceptionMapper<ConstraintViolationException>统一格式化错误响应结构,返回给前端清晰的字段错误列表
内容的提问来源于stack exchange,提问作者Dave
相关产品推荐
相关产品推荐

