1. Overview
In the following tutorial, we’ll learn the utility of the List variant of the annotations available under the package jakarta.validations.constraints. This annotation helps apply similar types of validations on a field. This also enables us to show different validation messages on the same field.
Let’s understand this in detail with a use case and its implementation.
2. Use Case Details
In this use case, we are going to develop a Java program for validating the fields captured as part of a job application form. The job aspirants can apply for different job levels by entering their personal details like name, years of experience, passport expiry date, etc. The program must validate the fields as shown below:
Field Name
Validations
Annotation
name
Only alphabets allowed
No consecutive spaces allowed
The first letter has to be in upper case
No space in the beginning
No space at the end
Less than 5 characters are not allowed
More than 20 characters are not allowed
Must show specific error messages for the above validation failures
experience
Years of experience in Junior Level Job
Min – 5
Max – 10
Years of experience in Mid-Senior Level Job
Min – 10
Max – 15
Years of experience in Senior Level Job
Min – 15
Max – 20
Must show specific error messages for the above validation failures for different job levels
agreement
All aspirants must give their consent to the agreement and terms of conditions
Must show specific error messages for different job levels
passport expiry date
Cannot be in the past
Must show specific error messages for different Job Levels
Usually, @Size, @Pattern, @Max, @Min, @AssertTrue, and @Future annotations of the package jakarta.validation.constraints work pretty well when it comes to displaying generic messages for validation failures. But the List variant of those annotations must be used to meet the last validation requirement, as highlighted above in the table for each of the fields.
Without waiting any more, let’s jump into the implementation.
3. Use Case Implementation
3.1. Overview
For implementing the scenarios discussed in section 2, we create a JobAspirant bean. This bean does 90% of the heavy lifting. All the fields are defined and decorated with the annotations supporting their validations. There is one more important attribute called groups in the annotations used below. It plays a crucial role in applying validations in specific contexts where they are needed.
Here we are not going to go through the prerequisites on how to use the validation framework. Also, the explanation given in this article should give a great headstart for the readers to explore more on the other annotations of similar nature.
So, in the subsequent sections, let’s go through the steps in applying the validations on each of the fields of the JobAspirant bean.
3.2. Marker Interfaces
The marker interfaces Senior.class, MidSenior.class, and Junior.class have played a significant role in applying the correct validation based on the job level. There is one more important marker interface, AllLevel.class, it’s like a universal signal saying “Hey, this validation must apply regardless of the job level!”. They are assigned to the groups attribute of the annotations to make the validations work appropriately. Let’s take a look at them:
public interface AllLevels {}
public interface Junior {}
public interface MidSenior {}
public interface Senior {}
3.3. Validate Name with @Size.List and @Pattern.List
For applying the different validations on the name field, we have used regex extensively with the @Pattern annotations in Pattern.List. Also, by using @Size annotation in Size.List we applied the min and max restriction on name. To understand this, let’s have a look at the annotations applied to the name field:
@Size.List({
@Size(min = 5, message = "Name should have at least 5 characters", groups = AllLevels.class),
@Size(max = 20, message = "Name should have at most 20 characters", groups = AllLevels.class)
})
@Pattern.List({
@Pattern(regexp = "^[\\p{Alpha} ]*$", message = "Name should contain only alphabets and space", groups = AllLevels.class),
@Pattern(regexp = "^[^\\s].*$", message = "Name should not start with space", groups = AllLevels.class),
@Pattern(regexp = "^.*[^\\s]$", message = "Name should not end with space", groups = AllLevels.class),
@Pattern(regexp = "^((?! ).)*$", message = "Name should not contain consecutive spaces", groups = AllLevels.class),
@Pattern(regexp = "^[^a-z].*$", message = "Name should not start with a lower case character", groups = AllLevels.class)
})
private String name;
Here is how we apply validation on the name field:
@Test
public void whenInvalidName_thenExpectErrors() throws ParseException {
JobAspirant jobAspirant = getJobAspirant("Senior", "John Adam", "2025-12-31", 17, true);
Set<ConstraintViolation<JobAspirant>> violations = validator.validate(jobAspirant, Senior.class, AllLevels.class);
assertThat(violations.size()).isEqualTo(1);
violations.forEach(action -> {
assertThat(action.getPropertyPath().toString()).isEqualTo("name");
assertThat(action.getMessage()).isEqualTo("Name should not contain consecutive spaces");
});
}
3.4. Validate Experience with @Min.List and @Max.List
We can apply different min and max years of experience criteria for various job levels using @Min.List and @Max.List. So, here are the annotations in the JobAspirant bean in action:
@Min.List({
@Min(value = 15, message = "Years of experience cannot be less than 15 Years", groups = Senior.class),
@Min(value = 10, message = "Years of experience cannot be less than 10 Years", groups = MidSenior.class),
@Min(value = 5, message = "Years of experience cannot be less than 5 Years", groups = Junior.class)
})
@Max.List({
@Max(value = 20, message = "Years of experience cannot be more than 20 Years", groups = Senior.class),
@Max(value = 15, message = "Years of experience cannot be more than 15 Years", groups = MidSenior.class),
@Max(value = 10, message = "Years of experience cannot be more than 10 Years", groups = Junior.class)
})
private Integer experience;
Let’s see the @Min annotations in action:
@Test
public void givenJobLevelJunior_whenInValidMinExperience_thenExpectErrors() throws ParseException {
JobAspirant jobAspirant = getJobAspirant("Junior", "John Adam", "2025-12-31", 3, true);
Set<ConstraintViolation<JobAspirant>> violations = validator.validate(jobAspirant, Junior.class);
assertThat(violations.size()).isEqualTo(1);
violations.forEach(action -> {
assertThat(action.getPropertyPath().toString()).isEqualTo("experience");
assertThat(action.getMessage()).isEqualTo("Years of experience cannot be less than 5 Years");
});
}
And now, here we check the @Max annotation’s role in the JobAspirant bean:
@Test
public void givenJobLevelJunior_whenInValidMaxExperience_thenExpectErrors() throws ParseException {
JobAspirant jobAspirant = getJobAspirant("Junior", "John Adam", "2025-12-31", 11, true);
Set<ConstraintViolation<JobAspirant>> violations = validator.validate(jobAspirant, Junior.class);
assertThat(violations.size()).isEqualTo(1);
violations.forEach(action -> {
assertThat(action.getPropertyPath().toString()).isEqualTo("experience");
assertThat(action.getMessage()).isEqualTo("Years of experience cannot be more than 10 Years");
});
}
3.5. Validate Agreement with @AssertTrue.List
Let’s take a look at the field agreement, which is a boolean. Usually, a boolean can be true or false, then what is so special about this validation? What if we want to show separate messages for different job levels? This is how we take care of this using @AssertTrue.List:
@AssertTrue.List({
@AssertTrue(message = "Terms and Conditions consent missing for Senior Level Job Application", groups = Senior.class),
@AssertTrue(message = "Terms and Conditions consent missing for Mid-Senior Level Job Application", groups = MidSenior.class),
@AssertTrue(message = "Terms and Conditions consent missing for Junior Level Job Application", groups = Junior.class)
})
private Boolean agreement;
Let’s check out validation on the agreement field for a senior-level job in action:
@Test
public void givenSeniorLevel_whenInvalidAgreement_thenExpectErrors() throws ParseException {
JobAspirant jobAspirant = getJobAspirant("Senior", "John Adam", "2025-12-31", 17, false);
Set<ConstraintViolation<JobAspirant>> violations = validator.validate(jobAspirant, Senior.class);
assertThat(violations.size()).isEqualTo(1);
violations.forEach(action -> {
assertThat(action.getPropertyPath().toString()).isEqualTo("agreement");
assertThat(action.getMessage()).isEqualTo("Terms and Conditions consent missing for Senior Level Job");
});
}
3.6. Validate Passport Expiry Date with @Future.List
Similarly, this kind of validation can be extended to fields of other types as well, such as passportExpiryDate, which is of type Date. To ensure that we show separate messages for expired passports for different job levels, we make use of @Future.List:
@Future.List({
@Future(message = "Active passport is mandatory for Senior Level Job Application", groups = Senior.class),
@Future(message = "Active passport is mandatory for Mid-Senior Level Job Application", groups = MidSenior.class),
@Future(message = "Active passport is mandatory for Junior Level Job Application", groups = Junior.class)
})
private Date passportExpiryDate;
Here is the annotation in action:
@Test
public void givenJobLevelMidSenior_whenInvalidPassport_thenExpectErrors() throws ParseException {
JobAspirant jobAspirant = getJobAspirant("MidSenior", "John Adam", "2021-12-31", 12, true);
Set<ConstraintViolation<JobAspirant>> violations = validator.validate(jobAspirant, MidSenior.class);
assertThat(violations.size()).isEqualTo(1);
violations.forEach(action -> {
assertThat(action.getPropertyPath().toString()).isEqualTo("passportExpiryDate");
assertThat(action.getMessage()).isEqualTo("Active passport is mandatory for Mid-Senior Level Job");
});
}
4. Conclusion
In this tutorial, we learned to use the List variant of the validation annotation along with the groups attribute to effectively apply different validation on the same field in different contexts. Moreover, the examples given here are good enough for the readers to explore more on other List annotations on their own.
As usual, all the examples shown in this article are available over on GitHub.