I have some code here with a ConstraintValidator that validates an object by comparing two of its attributes.
Here is the object, the annotation that is of interest to us is @ValidSmsTextLength:
@ValidSmsTextLength(groups = { PostGroup.class, PatchGroup.class, PostMessageCampaignGroup.class })
@JsonDeserialize(as = SmsMessageDto.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({"text", "encoding", "messagePartCount", "length"})
public class SmsMessageDto extends AbstractRestDto implements OneOfMessage {
@NotEmpty(message = "SMS_TEXT_NULL_OR_EMPTY", groups = { PostGroup.class, PatchGroup.class })
@JsonProperty("text")
private String text = null;
@ValidParameterByEnum(enumValid = EncodingEnum.class, message = "INVALID_ENCODING_ENUM", groups = {PostGroup.class, PostMessageCampaignGroup.class})
@JsonProperty("encoding")
private EncodingEnum encoding = EncodingEnum.GSM7;
This object SmsMessageDto, which implements OneOfMessage, is an attribute (named "body") of that object:
public class AbstractMessageDto extends AbstractRestResourceDto {
@JsonDeserialize(using = OneOfMessageDtoDeserializer.class)
@Valid
@NotNull(message = "MESSAGE_NULL", groups = PostGroup.class)
@JsonProperty("body")
protected OneOfMessage body = null;
And here are the Interface and ConstraintValidator associated with the annotation @ValidSmsTextLength:
@Constraint(validatedBy = SmsTextLengthValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidSmsTextLength {
String message() default "DEFAULT_SMS_TEXT_LENGTH_MESSAGE";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SmsTextLengthValidator implements ConstraintValidator<ValidSmsTextLength, SmsMessageDto> {
private static final String TEXT = "text";
@Override
public boolean isValid(SmsMessageDto smsMessageDto, ConstraintValidatorContext constraintValidatorContext) {
EncodingEnum encodingEnum = smsMessageDto.getEncoding();
if (smsMessageDto.getText() != null && EncodingEnum.GSM7.equals(encodingEnum) && smsMessageDto.getText().length() > 1530) {
constraintValidatorContext
.buildConstraintViolationWithTemplate("SMS_TEXT_LENGTH_GSM7_ERROR")
.addPropertyNode(TEXT)
.addConstraintViolation();
return false;
} else if (smsMessageDto.getText() != null && EncodingEnum.UNICODE.equals(encodingEnum) && smsMessageDto.getText().length() > 670) {
constraintValidatorContext
.buildConstraintViolationWithTemplate("SMS_TEXT_LENGTH_UNICODE_ERROR")
.addPropertyNode(TEXT)
.addConstraintViolation();
return false;
}
return true;
}
}
I implemented a test where I validate (through a @Validated annotated controller method) a AbstractMessageDto object which has a text attribute that satisfy the first if() condition in the isValid() method.
The BindingResult object present in my controller method ends up containing two errors:
- one that has been created with the default message ("DEFAULT_SMS_TEXT_LENGTH_MESSAGE"), where the field is "body".
- and one created with the correct isValid() message ("SMS_TEXT_LENGTH_GSM7_ERROR" for my test), where the field is "body.text" (because of the addPropertyNode(TEXT) in isValid()).
I would like for my BindingResult to not contain the error initialized with the default message. How can I stop the ConstraintValidator from creating that error and adding it to the BindingResult object?
CodePudding user response:
You need to call context.disableDefaultConstraintViolation()
.
One more suggestion for your validator: if the smsMessageDto
is null
, you will get a NullPointerException
, because the @NotNull
hasn't caused an error yet (the validation is still taking place). Add the following as first lines of the method:
if (smsMessageDto == null) {
// handled by @NotNull
return true;
}