Java Quickies – Hibernate Validator, reference implementation of JSR 303

In java, the Bean Validation specification is defined by the JSR 303. Hibernate Validator is the reference implementation for this JSR.
The validation API is available using the maven dependency:

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

At the time I am writing this article, the last version is 1.1.0.Final. This version has been used by Hibernate Validator since version 5. Before this revision, the API version was 1.0.0.GA and it was implemented by Hibernate Validator version 4. You actually do not need to add the API dependency in your pom.xml as it is already defined as a transitive dependency in Hibernate Validator :

  	<dependency>
  	  <groupId>org.hibernate</groupId>
  	  <artifactId>hibernate-validator</artifactId>
  	  <version>5.1.2.Final</version>
  	</dependency>

This is the only dependency needed to validate your beans inside a JEE container. If you want to use this validation implementation in a Java SE environment, you need to add these two dependencies :

    <dependency>
      <groupId>javax.el</groupId>
      <artifactId>javax.el-api</artifactId>
      <version>2.2.4</version>
    </dependency>
    <dependency>
      <groupId>org.glassfish.web</groupId>
      <artifactId>javax.el</artifactId>
      <version>2.2.4</version>
    </dependency>

The JSR provides some annotations that you can use to validate your beans. For instance, there are @NotNull, @Size, @Past, @Future, etc. Besides, Hibernate Validator adds its own annotations, we can find @NotBlank, @Length, @Email or even @CreditCard.
Here is what an annotated class looks like:

package io.resourcepool.model;

import java.util.Date;
import javax.validation.constraints.Past;
import org.hibernate.validator.constraints.*;

public class Client {

  private Long id;
  
  @NotBlank
  private String firstName;
  
  @NotBlank
  private String lastName;
  
  @Email
  private String email;
  
  @Past
  private Date birthDate;
  // Getters & Setters
}

How to validate with Hibernate Validator

It is quite easy and readable. The question is how can I validate my Bean now? Here again, it is quite easy. The JSR provides the interface javax.validation.Validator which declares the validate method.

public class ClientService {
  
  private Validator validator;
  
  public ClientService() {
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    validator = factory.getValidator();
  }
  
  private void validateClient(Client client) {
    Set<ConstraintViolation<Client>> violations =  validator.validate(client);
    // Process errors if any
  }
}

Now if I use my validator against this Object :

    Client client = new Client();
    client.setFirstName("Yoann");
    client.setEmail("Yoann");
    service.validateClient(client);

I get the following result :

ConstraintViolationImpl{interpolatedMessage='not a well-formed email address', propertyPath=email, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{org.hibernate.validator.constraints.Email.message}'}
ConstraintViolationImpl{interpolatedMessage='may not be empty', propertyPath=lastName, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{org.hibernate.validator.constraints.NotBlank.message}'}

Here I just displayed as a toString the ConstraintViolation<T> Object. As you can see, Constraints only check one thing. For instance, I did not give a birthDate, yet I did not get any error. If I wanted to have this field as required and in the past, I should have annotated the field with two annotations:

  @Email
  @NotNull
  private String email;
  
  @Past
  @NotNull
  private Date birthDate; 

Now I get this result :

ConstraintViolationImpl{interpolatedMessage='not a well-formed email address', propertyPath=email, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{org.hibernate.validator.constraints.Email.message}'}
ConstraintViolationImpl{interpolatedMessage='may not be null', propertyPath=birthDate, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{javax.validation.constraints.NotNull.message}'}
ConstraintViolationImpl{interpolatedMessage='may not be empty', propertyPath=lastName, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{org.hibernate.validator.constraints.NotBlank.message}'}

Let’s see the ContraintViolation class in details:

  • interpolatedMessage : This is a user friendly message.
  • propertyPath : This is the property which was not valid. The property can have a complex path such as: client.diplomas[0] in the case a Client had a list of diplomas. 0 is the indice of the failing diploma.
  • rootBeanClass : Gives informations about the class.
  • messageTemplate : This is a kind of error code which is used to get the user friendly message. Notice how the curly braces look like Unified Expression Language (JSR 341). It actually is exactly EL and this is the reason why outside of a JEE container you need two more dependencies.

Playing with the error messages

What we saw is pretty cool, but we can only handle simple validations. Let’s complicate things a little. How about we set a custom error message rather than a generic one? We can actually do it directly inside the annotation :

  @Past
  @NotNull(message = "You must set your date of birth")
  private Date birthDate;

This message supports EL.
Let’s try a more complex example, we have now added this class to our model:

package io.resourcepool.model;

import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.NotBlank;

public class Address {
  
  private String line1;
  
  private String line2;
  
  private String line3;
  
  @Pattern(regexp = "[0-9]{5}", message = "Invalid zip code!")
  private String zipCode; 
  
  @NotBlank
  private String city;
  
  @NotBlank
  private String country; 
  // Getters & Setters

}

Now our client has a list of addresses. We can say there is one for the delivery and another one for the invoice:

  @Size(min = 2, max = 2, message = "You need at least {min} addresses")
  @NotNull
  @Valid
  private List<Address> addresses;

First of all, if we do not add the @NotNull annotation, the addresses could be null and the @Size annotation would not even be checked. Also, you can see EL in action with the @Size error message:

ConstraintViolationImpl{interpolatedMessage='You need at least 2 addresses', propertyPath=addresses, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='You need at least {min} addresses'}

The {min} is replaced by the actual “min” value of the annotation.

Validate child objects with Hibernate Validator

The @Valid annotation allows to go through the child objet to check its constraints:

    List<Address> addresses = new ArrayList<Address>();
    Address address = new Address();
    address.setZipCode("00000");
    address.add(addresse);
    client.setAddresses(addresses);
    service.validateClient(client);

If I validate the client and only the client, it will also validate the address:

ConstraintViolationImpl{interpolatedMessage='may not be empty', propertyPath=addresses[0].country, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{org.hibernate.validator.constraints.NotBlank.message}'}
ConstraintViolationImpl{interpolatedMessage='You must set your date of birth', propertyPath=birthDate, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='You must set your date of birth'}
ConstraintViolationImpl{interpolatedMessage='You need at least 2 addresses', propertyPath=addresses, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='You need at least {min} addresses'}
ConstraintViolationImpl{interpolatedMessage='not a well-formed email address', propertyPath=email, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{org.hibernate.validator.constraints.Email.message}'}
ConstraintViolationImpl{interpolatedMessage='may not be empty', propertyPath=addresses[0].city, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{org.hibernate.validator.constraints.NotBlank.message}'}

Notice how the propertyPath is translated: “addresses[0].city”.

Custom Annotations with Hibernate Validator

Ok, we have seen more stuff, but we still are quite limited. Let’s see what custom annotations can bring us. We will add an annotation that will check if at least one line of the address is filled.

package io.resourcepool.validator.constraint;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

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

import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Target({ TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = AtLeastOneConstraint.AtLeastOneValidator.class)
public @interface AtLeastOneConstraint {

  String message() default "{io.resourcepool.validator.AtLeastOneConstraint.message}";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  String[] fieldNames();

  public static class AtLeastOneValidator implements ConstraintValidator<AtLeastOneConstraint, Object> {

    private static final Logger LOGGER = LoggerFactory.getLogger(AtLeastOneValidator.class);

    private String[] fieldNames;

    public void initialize(AtLeastOneConstraint constraintAnnotation) {
      this.fieldNames = constraintAnnotation.fieldNames();
    }

    public boolean isValid(Object object, ConstraintValidatorContext constraintContext) {
      if (object == null) {
        return true;
      }
      try {
        for (String field : fieldNames) {
          Object property = tryGetField(object, field, "get", "is");
          if (property != null) {
            return true;
          }
        }
      } catch (Exception e) {
        LOGGER.error("Exception during AtLeastOne validation", e);
      }
      return false;
    }

    private Object tryGetField(Object object, String field, String... prefixes) 
        throws IllegalAccessException, InvocationTargetException {
      for (String prefix: prefixes) {
        try {
          return object.getClass().getMethod(prefix + field.substring(0, 1).toUpperCase() + field.substring(1))
              .invoke(object);
        } catch (NoSuchMethodException ignore) {
          // Nothing to do
        }
      }
      return null;
    }
  }

}
 

A Hibernate Annotation has 3 mandatory fields:

  • String message() : This is the messageTemplate generated on error.
  • Class<?>[] groups() : This allows to do conditional validation. We will see exactly how later in this article.
  • Class<? extends Payload>[] : This allows to pass some additional information in case of constraint violation. We will not use this here. Keep in mind that if you omit this, you will get a RuntimeException.
  • The field “fieldNames” is a custom field added for our annotation needs. This is how you can use it:

    @AtLeastOneConstraint(fieldNames = { "line1", "line2", "line3" })
    public class Address
    

    This annotation has to be at the top of a class. This is required because of the @Target({ TYPE }) annotation. If we wanted to put it on a field, we could have used @Target({ FIELD }). Actually, this is an array, so we can put multiple elements @Target({TYPE, FIELD, METHOD}).
    And, that’s it for the Constraint. For each Constraint, there is a Validator that will be executed to check if the constraint is valid or not. This validator must be set in the @Constraint(validatedBy = MyClass.class) annotation. In our example, we pass a list of fields. First thing to do is to get the custom parameters in the validator. This is what is done in the initialize method.
    Next the isValid method is called. Now, we just go through the fields and try to obtain their getters using reflexion. That is it, we will execute getLine1(), getLine2(), getLine3(). If at least one of this field is not null, then the validation will be a success. Also note that is the object is null, we set the constraint as valid. This is a convention, as I said earlier, a constraint will only check one thing and we have nothing to do if the object is null. Remember the @Size annotation?
    Now if I try my brand new custom constraint, this is what I get:

    ConstraintViolationImpl{interpolatedMessage='{io.resourcepool.validator.AtLeastOneConstraint.message}', propertyPath=addresses[0], rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{io.resourcepool.validator.AtLeastOneConstraint.message}'}
    

    You need to notice two things. Firstly, the interpolatedMessage did not get interpolated. Indeed, Hibernate Validator has no way to know how to replace the message template. Fortunatly, it is easy to fix. Hibernate Validator will scan the class path for the ValidationMessages.properties file. This file can be localized. As an example, if I wanted to add French error messages, I would need to add the “ValidationMessages_fr.properties” file. We also can override Hibernate Validator build-in messages.
    If I add:

    io.resourcepool.validator.AtLeastOneConstraint.message=You need at least one of these fields.
    

    I get:

    ConstraintViolationImpl{interpolatedMessage='You need at least one of these fields.', propertyPath=addresses[0], rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{io.resourcepool.validator.AtLeastOneConstraint.message}'}
    

    Everything is going well except that… Oh, the propertyPath is “addresses[0]” and not the three fields line1, 2 and 3. This is normal because the constraint is set on the class. But we would definetly like to have the message linked to our 3 fields.
    We will add an utility class that we will improve through this article.

    package io.resourcepool.validator;
    
    public class ValidatorUtil {
        
      public static boolean linkErrorToProperty(String property, ConstraintValidatorContext context) {
        return linkErrorToProperty(property, context, false);
      }
      
      public static boolean linkErrorToProperty(String property, ConstraintValidatorContext context, boolean isValid) {
        if (isValid) {
          // Nothing to do if it is valid !
          return true;
        }
        // If it is invalid, we need to link it to the correct field
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
          .addPropertyNode(property).addConstraintViolation();
        return false;
      }
      
    }
    

    This is easy enough to understand. Note that if you do not call the addConstraintViolation, you will not get anything. This code is working using the validation-api 1.1. If you want to run it using the 1.0, you need to replace the addPropertyNode call by an addNode one.
    Going back to our validator:

    public static class AtLeastOneValidator implements ConstraintValidator<AtLeastOneConstraint, Object> {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(AtLeastOneValidator.class);
    
        private String[] fieldNames;
    
        public void initialize(AtLeastOneConstraint constraintAnnotation) {
          this.fieldNames = constraintAnnotation.fieldNames();
        }
    
        public boolean isValid(Object object, ConstraintValidatorContext constraintContext) {
          if (subValidation(object)) {
            return true;
          }
          // We need to link the error to the correct fields
          for (String field : fieldNames) {
            ValidatorUtil.linkErrorToProperty(field, constraintContext);
          }
          return false;
        }
    
        private boolean subValidation(Object object) {
          if (object == null) {
            return true;
          }
    
          try {
            for (String field : fieldNames) {
              Object property = tryGetField(object, field, "get", "is");
              if (property != null) {
                return true;
              }
            }
          } catch (Exception e) {
            LOGGER.error("Exception during AtLeastOneValidator validation", e);
          }
          return false;
        }
    
        private Object tryGetField(Object object, String field, String... prefixes) 
            throws IllegalAccessException, InvocationTargetException {
          for (String prefix: prefixes) {
            try {
              return object.getClass().getMethod(prefix + field.substring(0, 1).toUpperCase() + field.substring(1))
                  .invoke(object);
            } catch (NoSuchMethodException ignore) {
              // Nothing to do!
            }
          }
          return null;
        }
      }
    

    I moved the actual validation inside a “subValidation” method. This way, I can easily redirect the error in case it is invalid. Here is the result:

    ConstraintViolationImpl{interpolatedMessage='You need at least one of these fields.', propertyPath=addresses[0].line3, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{io.resourcepool.validator.AtLeastOneConstraint.message}'}
    ConstraintViolationImpl{interpolatedMessage='You need at least one of these fields.', propertyPath=addresses[0].line2, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{io.resourcepool.validator.AtLeastOneConstraint.message}'}
    ConstraintViolationImpl{interpolatedMessage='You need at least one of these fields.', propertyPath=addresses[0].line1, rootBeanClass=class io.resourcepool.model.Client, messageTemplate='{io.resourcepool.validator.AtLeastOneConstraint.message}'}
     

    The usefulness of groups to validate

    Let’s see what is the big deal about groups now. They are used to do conditional validation. Imagine you want to do a partial validation for client.
    First of all, we need to create an interface that we will use as a group. Groups can only be Interfaces:

    package io.resourcepool.validator;
    
    public interface ClientBase {
    }
    

    The Client class:

    package io.resourcepool.model;
    
    import java.util.Date;
    import java.util.List;
    
    import javax.validation.Valid;
    import javax.validation.constraints.*;
    
    import org.hibernate.validator.constraints.*;
    import io.resourcepool.validator.ClientBase;
    
    public class Client {
    
      private Long id;
      
      @NotBlank(groups = ClientBase.class)
      private String firstName;
      
      @NotBlank(groups = ClientBase.class)
      private String lastName;
      
      @Email(groups = ClientBase.class)
      @NotNull(groups = ClientBase.class)
      private String email;
      
      @Past(groups = ClientBase.class)
      @NotNull(groups = ClientBase.class, message = "You must set your date of birth")
      private Date birthDate;
      
      @Size(groups = ClientBase.class, min = 2, max = 2, message = "You need at least {min} addresses")
      @NotNull(groups = ClientBase.class)
      @Valid
      private List<Address> addresses;
    }
    

    If I try to validate the client the way we used to, you will notice that no violation are generated. Even if they should! Actually, the validate method can take a Class<?> var-args as second argument… Thus, we can change the method call as follow:

        Set<ConstraintViolation<Client>> violations = validator.validate(client, ClientBase.class);
    

    Doing so, I get my Client violations back, but I still get nothing from Address. Actually, if you do not set any groups in the annotation declaration, Hibernate Validator will automatically assume that the group is javax.validation.Default. In my projects, I usually use the Default group as a universal constraint, i.e. a constraint about the Length of the field inside the databse, or the email format. Thus, I am not forcing a field to be filled, but if there is something inside, the value needs to be correct. I also use a ValidatorUtil that will always add the Default group before validating:

    public class ValidatorUtil {
    
      private static Validator validator;
      
      static {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
      }
      
      public static <T> Map<String, String> validate(T object, Class<?>... groups) {
        Class<?>[] args = Arrays.copyOf(groups, groups.length + 1);
        args[groups.length] = Default.class;
        return extractViolations(validator.validate(object, args));
      }
      
      private static <T> Map<String, String> extractViolations(Set<ConstraintViolation<T>> violations) {
        Map<String, String> errors = new HashMap<String, String>();
        for (ConstraintViolation<T> v: violations) {
          errors.put(v.getPropertyPath().toString(), v.getMessage());
        }
        return errors;
      }
    
      public static boolean linkErrorToProperty(String property, ConstraintValidatorContext context) {
        return linkErrorToProperty(property, context, false);
      }
      
      public static boolean linkErrorToProperty(String property, ConstraintValidatorContext context, boolean isValid) {
        if (isValid) {
          // Nothing to do if it is valid !
          return true;
        }
        // If it is invalid, we need to link it to the correct field
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
          .addPropertyNode(property).addConstraintViolation();
        return false;
      }
      
    }
    

    The validator is thread-safe, so we can have only one instance in the whole application.

    Hibernate Validator and Spring

    We will now see how to use Hibernate Validator within a Spring context. In our example, we want to be sure that the email is unique. Here is the annotation and its validator:

    @Target({ TYPE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = { UniqueEmailConstraint.UniqueEmailValidator.class })
    public @interface UniqueEmailConstraint {
    
      String message() default "{io.resourcepool.validator.constraint.UniqueEmailConstraint.message}";
    
      Class<?>[] groups() default {};
    
      Class<? extends Payload>[] payload() default {};
      
      public static class UniqueEmailValidator implements ConstraintValidator<UniqueEmailConstraint, Client> {
        
        @Autowired
        private Client clientDao;
        
        @Override
        public void initialize(UniqueEmailConstraint constraintAnnotation) {
          // Nothing to do
        }
    
        @Override
        public boolean isValid(Client client, ConstraintValidatorContext context) {
          return ValidatorUtil.linkErrorToProperty("email", context, subValidation(client));
        }
        
        private boolean subValidation(Client client) {
          if (client == null || client.getEmail() == null) {
            return true;
          }
          Client res = clientDao.getByEmail(client.getEmail());
          // Valid if the email does not exist in DB or if it is already used by the current client
          return res == null || client.getId() != null && client.getId().equals(res.getId());
        }
      }
    }
    

    If you try to use the validator like this, you will notice that the @Autowired is not working. This is because Hibernate Validator has no knowledge about Spring. To make it work, we need to replace the default validator of hibernate by one provided by Spring: LocalValidatorFactoryBean! Here is how you can use it:

    	<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
    	
    	<bean id="ValidatorUtil" class="io.resourcepool.validator.ValidatorUtil">
    		<property name="validator" ref="validator"/>
    	</bean>
    

    How to separate a Constraint and its Validator

    Now we have seen a lot of stuff and we can almost handle any validations. But before we go further, we need to deal with something nasty Spring brought us… Our constraints are placed inside our model classes. The model is supposed to be the lowest layer and we really do not want to have a Spring dependency (@Autowired) or the DAO layer (ClientDao) at this level.
    What we would like is to be able to declare a constraint and put its validator inside another project. Let’s see how the api constraint are defined, remember, the API cannot point to an implementation:

    @Constraint(validatedBy = { })
    public @interface NotNull
    

    It is possible to separate the constraint and its validator. Oh by the way, from the beginning I have always been shipping the validator as an inner class of the constraint, this is not mandatory. So, we have to extract the d’UniqueEmailConstraint validator in another project, remove its reference in the validatedBy. And, as always we need to make Hibernate Validator aware of its existence. We need to add the constraints-client.xml file (you can name it whatever you wish):

    <constraint-mappings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                         xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/mapping validation-mapping-1.0.xsd"
                         xmlns="http://jboss.org/xml/ns/javax/validation/mapping">
        <constraint-definition annotation="io.resourcepool.validator.constraints.UniqueEmailConstraint">
            <validated-by include-existing-validators="false">
                <value>io.resourcepool.validator.UniqueEmailValidator</value>
            </validated-by>
        </constraint-definition>
    </constraint-mappings>
    

    It is possible to add as much “constraint-definition” as we would like. Unfortunetly Hibernate Validator will not magically find out about our file. We need to add the META-INF/validation.xml.

    <validation-config xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration">
        <constraint-mapping>/constraints-client.xml</constraint-mapping>
    </validation-config>
    

    Here again, it is possible to add as mush “constraint-mapping” as we would like. And yes, this file will be magically found out! Please note however that there can be only one META-INF/validation.xml file in the classpath!

    Cross-object validation

    This is the tougher case to handle. We will imagine a class Diploma which contains a completionDate. This diploma will be linked to a Client, but do not have access to the entity. Let’s say we want to validate the fact that the Client was at least 16 when he got his diploma (too bad for the gifted!).
    The first solution would be to add a clientBirthDate to our Diploma class. But that is not very classy, we would need to carry this property all the way when we want to only use it during validation…
    The second solution is actually close to the first. We could add the clientBirthDate field to a sub class of Diploma. Using Dozer, we could copy the whole object to the sub class and then validate the sub class.
    The last solution uses a validation context. We can add this inside our ValidatorUtil!

    public class ValidatorUtil {
    
      private Validator validator;
      
      private static ThreadLocal<Map<String, Object>> validationContext = new ThreadLocal<>();
      
      public void setValidator(Validator validator) {
        this.validator = validator;
      }
      
      public <T> Map<String, String> validate(T object, Class<?>... groups) {
        Class<?>[] args = Arrays.copyOf(groups, groups.length + 1);
        args[groups.length] = Default.class;
        return extractViolations(validator.validate(object, args));
      }
      
      public static <T> T getValidationVariable(String key) {
        // We are using containsKey not to throw an exception if the value is null
        if (validationContext.get() != null && validationContext.get().containsKey(key)) {
          return (T)validationContext.get().get(key);
        }
        throw new CannotValidateException(key + " doest not exist or the context is null!");
      }
      
      public static void addValidationVariable(String key, Object value) {
        if (validationContext.get() == null) {
          validationContext.set(new HashMap<String, Object>());
        }
        validationContext.get().put(key, value);
      }
      
      public static void clearValidationVariables() {
        validationContext.set(null);
      }
      
      public static boolean linkErrorToProperty(String property, ConstraintValidatorContext context) {
        return linkErrorToProperty(property, context, false);
      }
      
      public static boolean linkErrorToProperty(String property, ConstraintValidatorContext context, boolean isValid) {
        if (isValid) {
          return true;
        }
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
          .addPropertyNode(property).addConstraintViolation();
        return false;
      }
      
      private <T> Map<String, String> extractViolations(Set<ConstraintViolation<T>> violations) {
        Map<String, String> errors = new HashMap<String, String>();
        for (ConstraintViolation<T> v: violations) {
          errors.put(v.getPropertyPath().toString(), v.getMessage());
        }
        return errors;
      }
    }
    

    We will use ThreadLocal to protect our context. As long as our server does not switch thread during execution, we are safe. I have added 3 methods:

    • addValidationVariable : Will add a variable to the context.
    • clearValidationVariables : Delete the context.
    • getValidationVariable : Get a variable. By default, if the key is not found, I throw an exception to notify the user a variable is missing.

    Everything is accessible from a static context, this way, it is very easy to use it from a Validator.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.