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.
ConstraintValidatorand gather data from the exception in the handler. The downside is that you can get information only for one error like this, it does not work if you need to know about all errors for example.messageanderorrcodein that string instead of using marshalling. The method you use is for applying templates.ConstraintViolationcontains all the information you need. As it contains all the attributes from the annotation with their values in the, through accessing theConstraintDescriptor(which is basically the interpretation of the annotation on the field/class whatever you annotated). So you should convert theCOnstraintViolationExceptionyourself instead of letting Spring convert it to aMethodARgumentNotValidException(although I believe it transfers, some of, the information as well).