I want to create condition based validation in spring validator.
I have one UserDTO class in that there are two DTO class with #Valid annotation.
If I pass isPrimary true then it should validate only primaryDTO bean and ignoring secendoryDTO validations.
public class UserDTO {
#Valid
private PrimaryDTO primaryDTO;
#Valid
private SecendoryDTO secendoryDTO;
private boolean isPrimary;
}
public class PrimaryDTO {
#NotEmpty(message = "Please enter email.")
#Email(message = "Please enter a valid email.")
private String email;
}
public class SecendoryDTO {
#NotEmpty(message = "Please enter phone.")
private String phone;
}
Please Guide.
Thanks
If your validation depends on multiple fields (eg. isPrimary and either primaryDTO or secondaryDTO), then the only solution is to write a custom validator on classlevel (UserDTO) which will implement the conditional validation itself.
For example, create an annotation:
#Documented
#Retention(RUNTIME)
#Target({ANNOTATION_TYPE, TYPE})
#Constraint(validatedBy = SecondaryValidator.class)
public #interface ValidSecondary {
String message() default "Invalid secondary";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And create a validator that only validates the secondaryDTO field when isPrimary() is false:
#Component
public class SecondaryValidator implements ConstraintValidator<ValidSecondary, UserDTO> {
private Validator validator;
public SecondaryValidator(Validator validator) {
this.validator = validator;
}
#Override
public boolean isValid(UserDTO userDTO, ConstraintValidatorContext constraintValidatorContext) {
if (userDTO.isPrimary()) {
return true;
} else {
return validator.validate(userDTO.getSecondaryDTO()).isEmpty();
}
}
}
After that, you can remove the #Valid annotation from the secondaryDTO field and add the #ValidSecondary annotation on top of your UserDTO:
#ValidSecondary // Add this
public class UserDTO {
#Valid
private PrimaryDTO primaryDTO;
private SecondaryDTO secondaryDTO; // No more #Valid
private boolean primary;
}
However, in this case you'll lose any constraint violation message from within the SecondaryDTO, if you want to have some kind of passing through mechanism, you can add the violations to the constraintValidatorContext within the isValid() method, for example:
Set<ConstraintViolation<SecondaryDTO>> violations = validator.validate(userDTO.getSecondaryDTO());
violations.forEach(violation -> constraintValidatorContext
.buildConstraintViolationWithTemplate(violation.getMessage())
.addConstraintViolation());
Related
I've run into a situation where I need to validate a field inside an object conditionally. More specifically, I have one class PhoneType which contains two fields
#Getter
#Setter
public class PhoneType {
#JsonProperty("#CountryCode")
private String countryCode;
#JsonProperty("#Number")
private String number;
}
The class PhoneType is used in three places,
#Getter
#Setter
class PersonContact {
#JsonProperty("Mobile")
private PhoneType mobile;
#JsonProperty("WorkPhone")
private PhoneType workPhone;
#JsonProperty("OfficeFax")
private PhoneType officeFax;
}
However, with mobile, there should be an additional validation rule applied to the number field. The number must be a number with length of 10.
I have two possible solutions in mind:
Create a custom annotation to validate number for mobile
Validate number using Jackson's StdConverter
Here are the implementation of both solutions
public class ContactConverter extends StdConverter<PersonContact, PersonContact> {
#SneakyThrows
#Override
public PersonContact convert(PersonContact personContact) {
boolean validMobilePhone = Pattern.compile("\\d{10}")
.matcher(relatedPersonContact.getMobileNumber())
.matches();
if (BooleanUtils.isFalse(validMobilePhone)) {
var errorMessage = String.format(INVALID_MOBILE_NUMBER, personContact.getMobileNumber());
throw new JsonParseException(null, errorMessage);
}
return personContact;
}
}
Converter is used like this
#Getter
#Setter
#JsonDeserialize(converter = ContactConverter.class)
public class PersonContact {
#JsonProperty("#Email")
private String email;
#JsonProperty("WorkPhone")
private PhoneType workPhone;
#JsonProperty("Mobile")
private PhoneType mobile;
}
This is the code for custom annotation, however it's not working
#Retention(RUNTIME)
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
#Constraint(validatedBy = MobilePhoneNumberValidator.class)
#interface Phone {
String format() default "";
String message() default "Invalid phone number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
static class MobilePhoneNumberValidator implements ConstraintValidator<Phone, PhoneType> {
#Override
public void initialize(Phone constraintAnnotation) {
System.out.println("test");
}
#Override
public boolean isValid(PhoneType phoneType, ConstraintValidatorContext context) {
System.out.println("test again");
return false;
}
}
and this is how I use it
#Phone(format = "\\d{10}")
#JsonProperty("Mobile")
private PhoneType mobile;
However, the code inside the Validator is not executed.
I wonder if there is anything wrong with the custom annotation. This is SpringBoot 2.3.0, I can't think of any other reason why the custom annotation is not working.
Please help if you know there is a legit way handling dynamic annotation in Java, or you know why the above code isn't working, or you know a legit way of validating object's field just by name.
EDIT 1:
It seems like due to my poor way of explanation, there is misunderstanding.
https://www.baeldung.com/javax-validation-groups , this doesn't work in this case, the validation is applied only with the declaration of the PhoneType property in other classes (PersonContact)
I have two possible solutions, custom annotation and Jackson's converter.
I have successfully applied the converter but couldn't make the custom annotation work.
My custom annotation should be run after #JsonProperty, because it needs to have the field PhoneType mobile number to be deserialized.
I have the following controller:
public interface SaveController {
#PostMapping(value = "/save")
#ResponseStatus(code = HttpStatus.CREATED)
void save(#RequestBody #Valid SaveRequest saveRequest);
}
SaveRequest corresponds to:
public class SaveRequest {
#NotNull
private SaveType type;
private String name;
}
and SaveType:
public enum SaveType {
DAILY, WEEKLY, MONTHLY;
}
The controller does not receive the enum itself, but a camelCase String. I need to convert that String into the corresponding enum. For instance:
daily should become DAILY.
weekly should become WEEKLY.
monthly should become MONTHLY.
Any other String should become null.
I've tried using the Spring Converter class, which does not work when the enum is inside an object (at least I don't know how to make it work in such times).
I honestly don't know what else to try
https://www.baeldung.com/jackson-serialize-enums
This site should probably give you plenty of options.
Best is probably something like this:
public enum SaveType {
DAILY, WEEKLY, MONTHLY;
#JsonCreator
public static SaveType saveTypeforValue(String value) {
return SaveType.valueOf(value.toUpperCase());
}
}
What you require is to have custom annotation with a custom validation class for Enum.
javax.validation library doesn't have inbuilt support for enums.
Validation class
public class SaveTypeSubSetValidator implements ConstraintValidator<SaveTypeSubset, SaveType> {
private SaveType[] subset;
#Override
public void initialize(SaveTypeSubset constraint) {
this.subset = constraint.anyOf();
}
#Override
public boolean isValid(SaveType value, ConstraintValidatorContext context) {
return value == null || Arrays.asList(subset).contains(value);
}
}
interface for validation annotation with validation message
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = SaveTypeSubSetValidator.class)
public #interface SaveTypeSubset {
SaveType[] anyOf();
String message() default "must be any of {anyOf}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Usage
#SaveTypeSubset(anyOf = {SaveType.NEW, SaveType.OLD})
private SaveType SaveType;
This is one way. More ways are mentioned in this article.
I have the following class :
class ContactInformation {
String phone;
String email;
}
which is used in the following classes :
class Person {
#Valid
ContactInformation contactInformation;
}
class Station {
#Valid
ContactInformation contactInformation;
}
The thing is that any instance of Person must have an email, but it is an optional information for Station. Do I have a way to define this at owner level to avoid duplicate the class ContactInformation ?
Instead of the field level validator you can add the Type level validator.
Steps:
Define Type level annotation
Write Validator for new annotation
Introduce your type with a new annotation
Define:
#Constraint(validatedBy = {PersonClassOptionalEmailValidator.class})
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface PersonalEmailValid {
String message() default "Invalid Email address";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Writing Custom Validator:
public static class PersonClassOptionalEmailValidator implements ConstraintValidator<PersonalEmailValid, Person> {
// Test Email validator, you should check prope regex for production
public static final String EMAIL_REGEX = "^[a-zA-Z0-9+_.-]+#[a-zA-Z0-9.-]+$";
private final Pattern pattern;
public PersonClassOptionalEmailValidator() {
pattern = Pattern.compile(EMAIL_REGEX);
}
#Override
public boolean isValid(Person person, ConstraintValidatorContext constraintValidatorContext) {
if (person.contactInformation != null) {
return pattern.matcher(person.contactInformation.email).matches();
}
return false;
}
}
Introduce new annotation to class
#Getter
#Setter
#NoArgsConstructor
#PersonalEmailValid
static class Person {
#Valid
ContactInformation contactInformation;
}
Reference
Gist
Essentially, my service takes the role of streamlining the delivery of Email Notifications for standardization/control. I've therefore exposed a POST endpoint which takes in an email Bean as the response body which holds information such as receiver, sender, cc, etc and I would like to verify the fields of the incoming bean (i.e. email address format).
Currently, I have written a custom validator for validating a list of email addresses (#EmailAddresses). Is there a way to reuse the same validator to validate the email address for the "from" property which isn't a list as opposed to introducing another validator?
My Bean:
public class Email {
#JsonProperty("from")
private String from;
#EmailAddresses
#JsonProperty("to")
private List<String> to;
#EmailAddresses
#JsonProperty("cc")
private List<String> cc;
// some other fields
}
My Controller:
#RestController
public class MyController {
#RequestMapping(value = "/", method = RequestMethod.POST)
public String deliverEmailNotification(#Valid #RequestBody Email email) {
// something
}
}
My Custom Validation Annotation:
#Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Constraint(validatedBy = EmailAddressesValidator.class)
public #interface EmailAddresses {
String message() default "Must be a valid email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validation Implementation:
public class EmailAddressesValidator implements
ConstraintValidator<EmailAddresses, List<String>> {
#Override
public void initialize(EmailAddresses emailAddresses) {
// do nothing
}
#Override
public boolean isValid(final List<String> emails, ConstraintValidatorContext constraintValidatorContext) {
// do something
}
}
So essentially I'm wondering whether is it possible to do something like this:
public class EmailAddressesValidator implements
ConstraintValidator<EmailAddresses, List<String>>, ConstraintValidator<EmailAddresses, String> {
#Override
public void initialize(EmailAddresses emailAddresses) {
// do nothing
}
#Override
public boolean isValid(final List<String> emails, ConstraintValidatorContext constraintValidatorContext) {
// do something
}
#Override
public boolean isValid(final String email, ConstraintValidatorContext constraintValidatorContext) {
// do something
}
}
Or is there another way around it?
Didn't manage to implement two instances of the ConstraintValidator due to Duplicate class error. However, I was able to achieve the equivalent of overloading by having the validation interface be validated by two implementation classes.
Based on the field type annotated with the validation annotation (#EmailAddress in this case), the respective validation implementation will kick in.
#Target({ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Constraint(validatedBy = { EmailAddressValidator.class, EmailAddressesValidator.class })
public #interface EmailAddress {
String message() default "Must be a valid email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validation implementation for List of Strings
public class EmailAddressesValidator implements ConstraintValidator<EmailAddress, List<String>> {
...
}
Validation implementation for String
public class EmailAddressValidator implements ConstraintValidator<EmailAddress, String> {
...
}
I have a DTO that looks something like this:
class VehicleDto {
private String type;
private Car car;
private Bike bike;
}
Now depending on the type, I need to validate on at least one of Car and Bike.
Both cannot be present in the same request.
How can I do that?
Having two fields in class, while only one of them can present, seems like a design smell for me.
But if you insist on such design - you can create a custom Validator for your VehicleDto class.
public class VehicleValidator implements Validator {
public boolean supports(Class clazz) {
return VehicleDto.class.equals(clazz);
}
public void validate(Object obj, Errors errors) {
VehicleDto dto = (VehicleDto) obj;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "type",
"error.message.for.type.field");
if (null != dto.getType()
&& null != dto.getCar()
&& null != dto.getBike()) {
switch(dto.getType()) {
case "car":
errors.rejectValue("bike", "error.message.for.bike.field");
break;
case "bike":
errors.rejectValue("car", "error.message.for.car.field");
break;
}
}
}
}
Also, see Spring documentation about validation:
Validation using Spring’s Validator interface
Resolving codes to error messages
Injecting a Validator
For example, if we want to check whether my TaskDTO object is valid, by comparing its two attributes dueDate and repeatUntil , following are the steps to achieve it.
dependency in pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
DTO class:
#ValidTaskDTO
public class TaskDTO {
#FutureOrPresent
private ZonedDateTime dueDate;
#NotBlank(message = "Title cannot be null or blank")
private String title;
private String description;
#NotNull
private RecurrenceType recurrenceType;
#Future
private ZonedDateTime repeatUntil;
}
Custom Annotation:
#Constraint(validatedBy = {TaskDTOValidator.class})
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface ValidTaskDTO {
String message() default "Due date should not be greater than or equal to Repeat Until Date.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Constraint Validator:
public class TaskDTOValidator implements ConstraintValidator<ValidTaskDTO, TaskDTO> {
#Override
public void initialize(ValidTaskDTO constraintAnnotation) {
}
#Override
public boolean isValid(TaskDTO taskDTO, ConstraintValidatorContext constraintValidatorContext) {
if (taskDTO.getRecurrenceType() == RecurrenceType.NONE) {
return true;
}
return taskDTO.getRepeatUntil() != null && taskDTO.getDueDate().isBefore(taskDTO.getRepeatUntil());
}
}
Make sure that you have #Valid in front of RequestBody of a postmapping method in your RestController. Only then the validation will get invoked:
#PostMapping
public TaskReadDTO createTask(#Valid #RequestBody TaskDTO taskDTO) {
.....
}
I hope this helps. If you need a detailed explanation on steps, have a look at this video