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-ee
project and navigate to thesrc/main/java
- Create an interface called
SalaryConstraint
in theexpert.os.labs.persistence.validation
package -
Add the following annotations to the interface where:
- the
@Constraint
annotation come from thejakarta.validation
package - the other annotations come from the
java.lang.annotation
package
@Documented @Constraint(validatedBy = SalaryValidator.class) @Target( { ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME)
Warning
The
SalaryValidator.class
doesn'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 messagegroups
as the class group-
payload
as thePayload
from thejakarta.validation
package
Expected results
- A new class
SalaryConstraint
that 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
SalaryValidator
in theexpert.os.labs.persistence.validation
package -
Make the class implements the
ConstraintValidator
interface, typingSalaryConstraint
and theMoney
classes- the
ConstraintValidator
class comes from thejakarta.validation
package - the
Money
class comes from theorg.joda.money
package
- the
-
Implements the
isValid
method (because, probably, your IDE will complain about it!)Tip
Name the
Money
parameter assalary
for clarity purposes -
Add the validation logic in the
isValid
method containing:- check if the
salary
is not null - check if the
salary
is not negative or zero
Tips
- both checks can be connected to an
&&
operator - you can use the method
isNegativeOrZero()
from thevalue
field
- check if the
Expected results
- A new class
SalaryValidator
that 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
Worker
class - Add a new field called
salary
of typeMoney
from theorg.joda.money
package - Add the
@SalaryValidator
annotation to its field and associate themessage
attribute with the StringSalary should be greater than zero
- Add the field
salary
into the constructor - Add the getter and setter for the
salary
field - Add the field
salary
into thetoString()
method
Expected results
- The class
Worker
with a new fieldsalary
with 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
WorkerBuilder
class - Add a new field called
salary
of typeMoney
from theorg.joda.money
package - Add a new method for the
salary
field - Include the
salary
field into thebuild()
method
Expected results
- The class
WorkerBuilder
with a new fieldsalary
Solution
Click to see...
5. Create a test for the salary constraint
Steps
- Open the
WorkerTest
in thesrc/test/java
at theexpert.os.labs.persistence.validation
package - 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 zero
is present
- the negative value can be set using
Expected results
- A new test to validate the implementation of the custom validation for the
salary
field
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");
}