How to create custom Validation Messages based on an annotation property?

11,272

Solution 1

If your requirement can be satisfied by interpolating hibernate messages, so you can create/name your *property file like that:

ValidationMessages.properties

And inside that:

javax.validation.constraints.NotNull.message = CUSTOMIZED MESSAGE WHEN NOTNULL is violated!

Hibernate by default searches a ResourceBundle named ValidationMessages. Also a locale might be involved: ValidationMessages_en, ValidationMessages_de, <..>

Hibernate will provide your customized message through interpolatedMessage parameter, so all ConstraintViolationException relevant information (included your message ) will be showed. So you message will be a part of real exception. Some unwieldy information will be provided!

If you want to make your custom exception (without default ConstraintViolationException behavior) check this out:

Using GenericDao concept, consider the following

  public void saveOrUpdate(IEntity<?> entity) {
      try {
          if(entity.getId == null) {
            em.persist(entity);
          } else {
            em.merge(entity)l
          }
      } catch(ConstraintViolationException cve) {
          throw new ConstraintViolationEx(constructViolationMessage(cve.getConstraintViolations()));
      }
  }

 private String constructMessage(Set<ConstraintViolation<?>> pConstraintViolations) {
    StringBuilder customMessages = new StringBuilder();
    for(ConstraintViolation<?> violation : pConstraintViolations) {
        String targetAnnotation = violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName();
        if(supportsCustomMessage(targetAnnotation)) {
            applyMessage(violation, targetAnnotation, customMessages);
        } else {
            // do something with not customized constraints' messages e.g. append it to existing container
        }
    }
    return customMessages.toString();
 }

 private void applyMessage(ConstraintViolation<?> pViolation, String pTargetAnnotation, StringBuilder pCustomMessages) {
     String targetClass = pViolation.getRootBean().getClass().getName();
     String targetField = pViolation.getPropertyPath().toString();
     pCustomMessages.append(MessageFormat.format(getMessageByAnnotation(pTargetAnnotation), targetClass, targetField));
     pCustomMessages.append(System.getProperty("line.separator"));
 }


 private String getBundleKey() {
     return "ValidationMessages"; //FIXME: hardcoded - implement your true key
 }

 private String getMessageByAnnotation(String pTargetAnnotation) {
     ResourceBundle messages = ResourceBundle.getBundle(getBundleKey());
     switch(pTargetAnnotation) {
     case "NotNull":
         return messages.getString(pTargetAnnotation + ".message");
     default:
         return "";
     }
 }

 private boolean supportsCustomMessage(String pTargetAnnotation) {
     return customizedConstraintsTypes.contains(pTargetAnnotation);
 }

Produced result:

test.model.exceptions.ConstraintViolationEx
    test.model.Person : name cannot be null
    test.model.Person : surname cannot be null

A hibernate ConstraintViolation provides relevant information about root class and restricted field. As you see, it applies for all hibernate supported constraints, so you need to check if current annotation can be customized by supportsCustomMessage(<..>)! If it can (it's up to you), you should get appropriate message by constraint annotation doing `getMessageByAnnotation(<..>)'.

All you need to do is implement not supported constraints logic. For example it can append it's cause message or interpolated with default message (and true exception goes to *log file)

Solution 2

To customize your annotation message you need to disable the existing violation message inside isValid() method and build a new violation message and add it.

constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();

In the given below example, I am creating an annotation for input date validation on the basis of "invalid date", "can't be greater than today date" and "date format is correct or not".

@CheckDateIsValid(displayPattern = "DD/MM/YYYY", programPattern = "dd/MM/yyyy", groups = Order2.class)
    private String fromDate;

Annotation Interface -

public @interface CheckDateIsValid {

    String message() default "default message";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String displayPattern();

    String programPattern();
}

Annotation implementation Class -

    public class CheckDateIsValidValidator implements ConstraintValidator<CheckDateIsValid, String> {
    @Value("${app.country.timeZone}")
    private String timeZone;
    private String displayPattern;
    private String programPattern;

    @Override
    public void initialize(CheckDateIsValid constraintAnnotation) {
        this.displayPattern = constraintAnnotation.displayPattern();
        this.programPattern = constraintAnnotation.programPattern();
    }

    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        try {
            // disable existing violation message
            constraintContext.disableDefaultConstraintViolation();

            if (object == null) {
                return true;
            }

            final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(programPattern);
            LocalDateTime time = LocalDate.parse(object, formatter).atStartOfDay();

            ZoneOffset zoneOffSet = ZoneOffset.of(timeZone);
            OffsetDateTime todayDateTime = OffsetDateTime.now(zoneOffSet);

            if (time == null) {
                customMessageForValidation(constraintContext, "date is not valid");
                return false;
            } else if (todayDateTime.isBefore(time.atOffset(zoneOffSet))) {
                customMessageForValidation(constraintContext, "can't be greater than today date");
                return false;
            }

            return time != null;
        } catch (Exception e) {
            customMessageForValidation(constraintContext, "date format should be like " + displayPattern);
            return false;
        }
    }

    private void customMessageForValidation(ConstraintValidatorContext constraintContext, String message) {
        // build new violation message and add it
        constraintContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();
    }
}
Share:
11,272
chr0x
Author by

chr0x

Updated on June 13, 2022

Comments

  • chr0x
    chr0x almost 2 years

    I'm using the Hibernate @NotNull validator, and I'm trying to create custom messages to tell the user which field has generated the error when it is null. Something like this:

    notNull.custom = The field {0} can't be null.
    

    (this will be in my ValidationMessages.properties file).

    Where the {0} should be the field name passed to the validator this way:

    @NotNull(field="field name")
    

    There is any way I can do that?