Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic POJO validation based on groups in spring

Consider the following pojo for reference:

public class User{

    private  String username;
    private String firstName;
    private String middleName;
    private String lastName;
    private String phone;

    //getters and setters

}

My application is a basically spring-boot based REST API which exposes two endpoints, one to create the user and the other to retrieve a user.

The "users" fall into certain categories, group-a, group-b etc. which I get from the headers of the post request.

I need to validated the user data in runtime and the validations may differ based on the group of a user.

for example, the users that fall into group-a may have phone numbers as an optional field whereas it might be a mandatory field for some other group.

The regex may also vary based on their groups.

I need to be able to configure spring, to somehow dynamically validate my pojo as soon as they are created and their respective set of validations get triggered based on their groups.

Maybe I can create a yml/xml configuration which would allow me to enable this?

I would prefer to not annotate my private String phone with @NotNull and @Pattern.

My configuration is as follows:

public class NotNullValidator implements Validator {
    private String group;
    private Object target;

    public String getGroup() {
        return group;
    }

    public void setGroup(String group) {
        this.group = group;
    }

    public Object getTarget() {
        return target;
    }

    public void setTarget(Object target) {
        this.target = target;
    }

    @Override
    public void validate(Object o) {
        if (Objects.nonNull(o)) {
            throw new RuntimeException("Target is null");
        }
    }
}


public interface Validator {
    void validate(Object o);
}


@ConfigurationProperties(prefix = "not-null")
@Component
public class NotNullValidators {
    List<NotNullValidator> validators;

    public List<NotNullValidator> getValidators() {
        return validators;
    }

    public void setValidators(List<NotNullValidator> validators) {
        this.validators = validators;
    }
}


application.yml

not-null:
  validators:

    -
      group: group-a
      target: user.username

    -
      group: group-b
      target: user.phone

I want to configure my application to somehow allow the validators to pick their targets (the actual objects, not the strings mentioned in the yml), and invoke their respective public void validate(Object o) on their targets.

P.S.

Please feel free to edit the question to make it better.

I am using jackson for serializing and deserializing JSON.

like image 422
Adit A. Pillai Avatar asked Oct 05 '17 17:10

Adit A. Pillai


People also ask

Is @valid and @validated the same?

The @Valid annotation ensures the validation of the whole object. Importantly, it performs the validation of the whole object graph. However, this creates issues for scenarios needing only partial validation. On the other hand, we can use @Validated for group validation, including the above partial validation.

How do you validate the range of a number in Spring?

In Spring MVC Validation, we can validate the user's input within a number range. The following annotations are used to achieve number validation: @Min annotation - It is required to pass an integer value with @Min annotation. The user input must be equal to or greater than this value.

What is @validated annotation in Spring?

The @Validated annotation is a class-level annotation that we can use to tell Spring to validate parameters that are passed into a method of the annotated class. We'll learn more about how to use it in the section about validating path variables and request parameters.


2 Answers

The easiest solution to your problem, as i see it, is not with Spring or the POJOs themselves but with a design pattern.

The problem you're describing is easily solved by a strategy pattern solution.

You match the strategy to use by the header you're expecting in the request, that describes the type of user, and then you perform said validations inside the strategy itself.

This will allow you to use the same POJO for the whole approach, and deal with the specifics of handling/parsing and validating data according to the each type of user's strategy.

Here's a link from wiki books with a detailed explanation of the pattern

Strategy Pattern

Suppose you have a basic interface for your strategies:

interface Strategy { 

    boolean validate(User user);
}

And you have 2 different implementations for the 2 different types of user:

public class StrategyA implements Strategy {

    public boolean validate(User user){

         return user.getUsername().isEmpty();
    }
}

public class StrategyB implements Strategy {

    public boolean validate(User user){

         return user.getPhone().isEmpty();
    }
}

You add a Strategy attribute to your User POJO and assign the right implementation of the Strategy to that attribute when you receive the post request.

Everytime you need to validate data for that user you just have to invoke the validate method of the assigned strategy.

If each User can fit multiple strategies, you can add a List<Strategy> as an attribute instead of a single one.

If you don't want to change the POJO you have to check which is the correct strategy every time you receive a post request.

Besides the validate method you can add methods to handle data, specific to each strategy.

Hope this helps.

like image 108
ricol070 Avatar answered Oct 29 '22 10:10

ricol070


You can use validation groups to control which type of user which field gets validated for. For example:

@NotBlank(groups = {GroupB.class})
private String phone;

@NotBlank(groups = {GroupA.class, GroupB.class})
private String username;

Then you use the headers from the request that you mentioned to decide which group to validate against.

See http://blog.codeleak.pl/2014/08/validation-groups-in-spring-mvc.html?m=1 for a complete example.


Updated to include a more comprehensive example:

public class Val {
    private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    public boolean isValid(User user, String userType) {
        usergroups userGroup = usergroups.valueOf(userType);
        Set<ConstraintViolation<User>> constraintViolations = validator.validate(user, userGroup.getValidationClass());
        return constraintViolations.isEmpty();
    }

    public interface GroupA {}
    public interface GroupB {}

    public enum usergroups {
        a(GroupA.class),
        b(GroupB.class);

        private final Class clazz;

        usergroups(Class clazz) {
            this.clazz = clazz;
        }

        public Class getValidationClass() {
            return clazz;
        }
    }
}

This doesn't use application.yaml, instead the mapping of which fields are validated for each group is set in annotations, similar results using Spring's built in validation support.

like image 43
SergeyB Avatar answered Oct 29 '22 09:10

SergeyB