Java Validation using Annotations

Validation in Java can be done nicely using annotations. Simply define annotations for your business rules then for each field you want to validate, simply annotate it with something and leave the validator to do the rest.

Let’s consider a class that models a phone bill. I want to validate it using these 3 rules:

  • Date is something not earlier than the first telephone company
  • Date is not something in the future, because we cannot tax people for future use
  • Date and also a sum of money must be present for a bill to be valid

So let’s make this class.

package net.superglobals;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class PhoneBill {
	private LocalDate date;
	private Double total;
	private DateTimeFormatter df = DateTimeFormatter.ofPattern("dd.MM.yyyy");
	
	public LocalDate getDate() {
		return date;
	}
	
	public void setDate(LocalDate date) {
		this.date = date;
	}
	
	public Double getTotal() {
		return total;
	}
	
	public void setTotal(Double total) {
		this.total = total;
	}

	@Override
	public String toString() {
		return "PhoneBill [date=" + date.format(df) + ", total=" + total + "]";
	}
}

But before going on we need two dependencies for our project. For validation in java using annotations we need.

Java Validation Api

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

And an implementation for this api, be it hibernate-validator or whatever any other library. We code against the api so we don’t care that much about the implementation library. I used hibernate-validator which also requires Expression Language (EL) support.

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.14.Final</version>
</dependency>
     
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.el</artifactId>
    <version>3.0.1-b09</version>
</dependency>

Validation using annotations in Java

You basically need to define an interface (a special one) and some “implementation” for it.

  • Target FIELD means that this annotation can go only above class fields
  • Retention RUNTIME means keep the annotation for reading during execution (read more here)
  • Constraint validatedBy indicates the class which will “implement” this
  • notice the @interface in the definition
package net.superglobals;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateValidImpl.class)
public @interface DateValid {
	String message() default "Weird date!";
	
	Class<?>[] groups() default {};
	
	Class<? extends Payload>[] payload() default {};
}

Btw, you can omit the default fields, but in this case you have to supply them as arguments to the annotation (uglier imho).

Now onto the implementation of the Java validation. We need to implement ConstraintValidator which takes two generic params:

  1. Your annotation
  2. The type of field you want to put this annotation on

Inside isValid you simply add your validation logic.

package net.superglobals;

import java.time.LocalDate;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class DateValidImpl implements ConstraintValidator<DateValid, LocalDate> {

	@Override
	public boolean isValid(LocalDate date, ConstraintValidatorContext context) {
		LocalDate lastCentury = LocalDate.of(1877, 6, 4);
		if (date.isBefore(lastCentury) || date.isAfter(LocalDate.now())) {
			context.disableDefaultConstraintViolation();
			context.buildConstraintViolationWithTemplate("Weird date: " + date + ". Either the phone was not invented that time or you just got off a DeLorean.")
				.addPropertyNode("date")
				.addConstraintViolation();
			return false;
		}
		
		return true;
	}

}

Let’s check what we have until now. Add the annotation to the field.

@DateValid
private LocalDate date;

Then we test it in main.

package net.superglobals;

import java.time.LocalDate;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

public class Main {
    public static void main(String[] args){
    	PhoneBill bill = new PhoneBill();
    	bill.setDate(LocalDate.of(1500,1,1));
    	bill.setTotal(500d);
    	ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Set<ConstraintViolation<PhoneBill>> validationResult = validator.validate(bill);
        
       for (ConstraintViolation<PhoneBill> c : validationResult) {
    	   System.out.println(c.getMessage() + " on field: " + c.getPropertyPath());
       }
    } 
}

And we get the following output.

Weird date: 1500-01-01. Either the phone was not invented that time or you just got off a DeLorean. on field: date.date

But I want not only the date to be valid but also have something in the total field. A bill needs to have a date but also a sum, be it 0 or negative. It cannot be null.

How can we make sure that two fields (or more) are valid at the same time, not just individually? Well, again the answer is: Java validation using annotations.

We create an annotation like the one above. This time Target is TYPE. Which means we need to add this at the top of a class.

package net.superglobals;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {PhoneBillValidImpl.class})
public @interface PhoneBillValid {
	String message() default "Inconsistent phone bill";
	
	Class<?>[] groups() default {};
	
	Class<? extends Payload>[] payload() default {};
}

And it’s implementation.

package net.superglobals;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PhoneBillValidImpl implements ConstraintValidator<PhoneBillValid, PhoneBill> {

	@Override
	public boolean isValid(PhoneBill bill, ConstraintValidatorContext context) {
		if (bill.getDate() == null) {
			context.disableDefaultConstraintViolation();
			context.buildConstraintViolationWithTemplate("Null date")
				.addPropertyNode("date")
				.addConstraintViolation();
			return false;
		}
		if (bill.getTotal() == null) {
			context.disableDefaultConstraintViolation();
			context.buildConstraintViolationWithTemplate("Null total")
			.addPropertyNode("total")
			.addConstraintViolation();
			return false;
		}
		return true;
	}
}

Add it at the top like this.

@PhoneBillValid
public class PhoneBill {
...

Test it inside main making the total null.

bill.setTotal(null);
FacebookTwitterLinkedin