Duplicate Validation Errors with @NotNull Annotation and Lombok Integration
When adding a new API endpoint with a request parameter annotated with @NotNull, duplicate validation error messages may appear in the response. This ocurs due to interactions between Lombko's annotation copying behavior and Hibernate Validator's constraint violation handling.
Defining the Data Transfer Object
Create a data transfer object with a field annotated with @NotNull and Lombok's @Data annotation.
@Data
public class RequestData {
@NotNull(message = "Content cannot be empty")
private String content;
}
Implementing the Controller
Define a controller with a method that validates the request body using @Valid.
@RestController
@Validated
public class ApiController {
@PostMapping("/submit")
public String process(@RequestBody @Valid RequestData data) {
return "Success";
}
}
Handling Validation Exceptions
Implement a global exception handler to process validation errors and return a structured response.
@RestControllerAdvice
public class ExceptionHandlerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseWrapper handleValidationError(MethodArgumentNotValidException exception) {
List<ObjectError> errors = exception.getBindingResult().getAllErrors();
String errorMessages = errors.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(", "));
return ResponseWrapper.error(400, errorMessages);
}
}
With this setup, calling the endpoint with an empty content field results in duplicate error messages: "Content cannot be empty, Content cannot be empty".
Temporary Workaround
To resolve the duplication, modify the exception handler to filter out duplicate errors.
@RestControllerAdvice
public class ExceptionHandlerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseWrapper handleValidationError(MethodArgumentNotValidException exception) {
List<ObjectError> uniqueErrors = exception.getBindingResult()
.getAllErrors()
.stream()
.distinct()
.collect(Collectors.toList());
String errorMessages = uniqueErrors.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(", "));
return ResponseWrapper.error(400, errorMessages);
}
}
After applying this change, the endpoint returns a single error message: "Content cannot be empty".
Root Cause Analysis
Lombok Annotation Copying
Lombok copies certain annotations from fields to generated getter and setter methods. In version 1.18.24, javax.validation.constraints.NotNull is included in the list of copyable annotations. This results in the @NotNull annotation being present on both the field and the getter method parameter, causing Hibernate Validator to validate the constraint twice.
Hibernate Validator Constraint Violation Handling
Hibernate Validator uses ConstraintViolationImpl objects to represent validation errors. These objects are stored in a Set within ValidationContext, which relies on hashCode for uniqueness.
In Hibernate Validator 6.0.22, the hashCode calculation includes the elementType field. Since the field and getter method have different element types (FIELD vs. GETTER), their corresponding ConstraintViolationImpl instances produce different hash codes, allowing duplicates in the set.
private int computeHashCode() {
int hash = (interpolatedMessage != null) ? interpolatedMessage.hashCode() : 0;
hash = 31 * hash + (propertyPath != null ? propertyPath.hashCode() : 0);
hash = 31 * hash + System.identityHashCode(rootBean);
hash = 31 * hash + System.identityHashCode(leafBean);
hash = 31 * hash + System.identityHashCode(value);
hash = 31 * hash + (constraintDescriptor != null ? constraintDescriptor.hashCode() : 0);
hash = 31 * hash + (messageTemplate != null ? messageTemplate.hashCode() : 0);
hash = 31 * hash + (elementType != null ? elementType.hashCode() : 0);
return hash;
}
In Hibernate Validator 6.2.0.Final, the elementType field is excluded from the hashCode calculation. This causes ConstraintViolationImpl instances for the field and getter method to have identical hash codes, ensuring only one instance is stored in the set and eliminating duplicate error messages.
Upgrading to Hibernate Validator 6.2.0.Final or later resolves the issue without requiring code changes in the exception handler.