Jakarta Validation - Lab 2
The goal is to illustrate how to create a custom validation in Java using Jakarta Validation.
We're developing a custom validation called SalaryValidator and an associated annotation named SalaryConstraint.
This custom validation ensures that the Money field representing an employee's salary in the Worker class is non-null and greater than zero.
This approach enhances data validation in Java applications, ensuring that salary values meet specific criteria and providing customized error messages for validation failures.
1. Create the custom constraint class
Steps
- Open the
01-jakarta-eeproject and navigate to thesrc/main/java - Create an interface called
SalaryConstraintin theexpert.os.labs.persistence.validationpackage -
Add the following annotations to the interface where:
- the
@Constraintannotation come from thejakarta.validationpackage - the other annotations come from the
java.lang.annotationpackage
@Documented @Constraint(validatedBy = SalaryValidator.class) @Target( { ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME)Warning
The
SalaryValidator.classdoesn't exist yet. We will create it in the next steps. For now, let your IDE complain about this missing class. - the
-
Define the annotation attributes:
message, as the default messagegroupsas the class group-
payloadas thePayloadfrom thejakarta.validationpackage
Expected results
- A new class
SalaryConstraintthat will be used for the custom validation
Solution
Click to see...
import jakarta.validation.Constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Constraint(validatedBy = SalaryValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SalaryConstraint {
}
2. Create the custom validator class
Steps
- Create a class
SalaryValidatorin theexpert.os.labs.persistence.validationpackage -
Make the class implements the
ConstraintValidatorinterface, typingSalaryConstraintand theMoneyclasses- the
ConstraintValidatorclass comes from thejakarta.validationpackage - the
Moneyclass comes from theorg.joda.moneypackage
- the
-
Implements the
isValidmethod (because, probably, your IDE will complain about it!)Tip
Name the
Moneyparameter assalaryfor clarity purposes -
Add the validation logic in the
isValidmethod containing:- check if the
salaryis not null - check if the
salaryis not negative or zero
Tips
- both checks can be connected to an
&&operator - you can use the method
isNegativeOrZero()from thevaluefield
- check if the
Expected results
- A new class
SalaryValidatorthat will be used to validate if the salary is not null and greater than zero
Solution
Click to see...
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.joda.money.Money;
public class SalaryValidator implements ConstraintValidator<SalaryConstraint, Money> {
@Override
public boolean isValid(Money salary, ConstraintValidatorContext constraintValidatorContext) {
return salary != null && salary.isNegativeOrZero();
}
}
3. Add the salary field to the Worker class
Steps
- Open the
Workerclass - Add a new field called
salaryof typeMoneyfrom theorg.joda.moneypackage - Add the
@SalaryValidatorannotation to its field and associate themessageattribute with the StringSalary should be greater than zero - Add the field
salaryinto the constructor - Add the getter and setter for the
salaryfield - Add the field
salaryinto thetoString()method
Expected results
- The class
Workerwith a new fieldsalarywith a custom constraint
Solution
Click to see...
public class Worker {
// previous fields ignored
@SalaryConstraint(message = "Salary should be greater than zero")
private Money salary;
public Worker(String nickname, String name, boolean working, String bio, int age, String email, Money salary) {
// previous attributes ignored
this.salary = salary;
}
public void setSalary(Money salary) {
this.salary = salary;
}
public void getSalary() {
return salary;
}
}
4. Add the salary field to the WorkerBuilder class
Steps
- Open the
WorkerBuilderclass - Add a new field called
salaryof typeMoneyfrom theorg.joda.moneypackage - Add a new method for the
salaryfield - Include the
salaryfield into thebuild()method
Expected results
- The class
WorkerBuilderwith a new fieldsalary
Solution
Click to see...
5. Create a test for the salary constraint
Steps
- Open the
WorkerTestin thesrc/test/javaat theexpert.os.labs.persistence.validationpackage - Add a new test called
shouldNotCreateWorkerWithNegativeSalary() - Implement the test setting a negative value to the
salary()method from the builder- the negative value can be set using
Money.of(CurrencyUnit.of("USD"), -10) - the assertion must validate if the message
Salary should be greater than zerois present
- the negative value can be set using
Expected results
- A new test to validate the implementation of the custom validation for the
salaryfield
Solution
Click to see...
@Test
void shouldNotCreateWorkerWithNegativeSalary() {
Worker worker = Worker.builder()
.nickname("jonhdoe")
.name("John Doe")
.working(true)
.bio("I am a software engineer")
.age(30)
.email("john.doe@gmail.com")
.salary(Money.of(CurrencyUnit.of("USD"), -10)).build();
Set<ConstraintViolation<Worker>> violations = validator.validate(worker);
Assertions.assertThat(violations).isNotEmpty().hasSize(1)
.map(ConstraintViolation::getMessage).contains("Salary should be greater than zero");
}