1. What I’m Trying to Achieve?
I wanted to have a custom annotation that supports the errorCode
along with the errorMessage
parameter.
@NotBlank(message = "Field X can't be Blank.", errorCode = 23442)
So, I create NotBlank
annotation with the below schema.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({FIELD, PARAMETER})
@Constraint(validatedBy = {NotBlankAnnotationProcessor.class})
public @interface NotBlank {
String message() default "{commons.validation.constraints.NotBlank.message}";
@SuppressWarnings("unused")
Class<?>[] groups() default {};
@SuppressWarnings("unused")
Class<? extends Payload>[] payload() default {};
long errorCode() default 400;
}
and it has an annotation processor for validation NotBlankAnnotationProcessor
@Getter
public class NotBlankAnnotationProcessor implements ConstraintValidator<NotBlank, String> {
private String message;
private long errorCode;
@Override
public void initialize(NotBlank annotation) {
message = annotation.message();
errorCode = annotation.errorCode();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
boolean isValid = !Strings.isBlank(value);
if (isValid) {
return true;
}
context.disableDefaultConstraintViolation();
var serializedJSON = getSerializedObject(getMessage(), getErrorCode()); //marshaling
context.buildConstraintViolationWithTemplate(serializedJSON).addConstraintViolation();
return false;
}
}
In the line, var serializedJSON = getSerializedObject(getMessage(), getErrorCode());
I'm marshaling the message into a JSON payload with the message
and errorCode
as there seems no way of throwing the custom exception with the custom body. In the exceptionHandler
I'm able to unmarshal the JSON string so that's why I'm marshaling and unmarshalling.
@ExceptionHandler
ResponseEntity<Map<String, Object>> handleMethodArgumentException(
MethodArgumentNotValidException exception) {
Map<String, Object> body = new LinkedHashMap<>(1, 1);
final List<ErrorStructure> errorStructures =
exception.getBindingResult().getFieldErrors().stream()
.map(error -> deserialize(error.getDefaultMessage()))
.toList();
body.put("messages", errorStructures);
return ResponseEntity.status(BAD_REQUEST).contentType(APPLICATION_JSON).body(body);
}
Utility class that I'm using for marshaling and unmarshalling.
public final class AnnotationProcessorToExceptionHandlerAdapter {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
//NON-INSTANTIABLE UTILITY CLASS.
private AnnotationProcessorToExceptionHandlerAdapter() {
throw new AssertionError("No instance for you!");
}
@SneakyThrows
public static String getSerializedObject(final String errorMessage, final long errorCode) {
ErrorStructure errorStructure = new ErrorStructure(errorMessage, errorCode);
return OBJECT_MAPPER.writeValueAsString(errorStructure);
}
@SneakyThrows
public static ErrorStructure deserialize(final String serializedValue) {
return OBJECT_MAPPER.readValue(serializedValue, ErrorStructure.class);
}
}
2. What Is the Problem?
While the solution works perfectly fine. However, I'm certain on the higher TPS (transaction per second) will cause performance issues as I am doing unnecessary marshaling and unmarshalling to work around.
3. Alternatively, How I Don’t Want to Do?
I don't want to include the BindingResults
in hundreds of function parameters in the current codebase something like the below and check errors everywhere.
void onSomething(@RequestBody @Validated WithdrawMoney withdrawal, BindingResult errors) {
if (errors.hasErrors()) {
throw new ValidationException(errors);
}
}
4. Recently Read Articles.
I've read the below answers:
A custom annotation can throw a custom exception, rather than MethodArgumentNotValidException?
How to make custom annotation throw an error instead of default message
How to throw custom exception in proper way when using @javax.validation.Valid?
Spring Boot how to return my own validation constraint error messages
Custom Validation Message in Spring Boot with internationalization?
Any help would be really appreciated!
Thanks.