Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Duplicate Validation Errors with @NotNull Annotation and Lombok Integration

Tech 2

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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.