Custom Validation in Java Spring

Viewed 584

I am pretty new with Java, and I have just created a spring boot application to practice a bit.

I created a controller that receives a POST request with json:

{
   "team1": {
     "score": 10,
     "colour": "RED"
   },
   "team2": {
     "score": 3,
     "colour": "BLUE"
   }

}

Controller:

public @ResponseBody
    ResponseEntity<Response<Game>> saveGame(
            @Valid @RequestBody PostedGameRequest postedGameRequest
    ) {
    // ...
}

The PostedGameRequest gets validated, and that works fine, but there are two properties: team1 and team2 of type PostedGameRequestTeam, and they don't get validated. I added some @NotEmpty attributes in there, but that didn't work.

So now I created a custom TeamValidator, that validates the fields in the Team, but when it fails, it will only show the message that I specified in the @Team attribute class. It would be nice if I could validate each field by itself.

@Builder
public class PostedGameRequest {
    @Team()
    private final PostedGameRequestTeam team1;

    @Team()
    private final PostedGameRequestTeam team2;

    public PostedGameRequestTeam getTeam1() {
        return team1;
    }

    public PostedGameRequestTeam getTeam2() {
        return team2;
    }

}
public class TeamValidator implements ConstraintValidator<Team, PostedGameRequestTeam> {

    List<String> colours = Arrays.asList("RED","BLUE");

    @Override
    public boolean isValid(@Valid PostedGameRequestTeam team, ConstraintValidatorContext context) {
        if (team == null || team.getColour() == null || team.getScore() == null) {
            return false;
        }

        if (team.getScore() < 0 || team.getScore() > 11) {
            // This will only lead to an invalid team message, should get a message about the score in this case
            return false;
        }

        if (!colours.contains(team.getColour())) {
            // This will only lead to an invalid team message, should get a message about the colour in this case
            return false;
        }

        return (team.getId() != null || team.getName() != null);
    }
}

I have the feeling I am not doing this as I am supposed to do it. Why did my first approach not work? Is there something I have to add that makes the validator also run for the team1 and team2 properties without me adding a custom Validator @Team?

2 Answers

Instead of @NotEmpty try with

@NotNull

This will make sure that team1 and team2 always contain an object which is not null

If you want more validation for each property of team1 and team2 get inside that object in java and add more annotations to those fields. For the string property you can add

@NotNull
@Size (min = 1)

which will make sure that it always contains a value

Also to get one step inside with validations just add @Valid So your object will become

public class PostedGameRequest {

    @NotNull
    @Valid
    private final PostedGameRequestTeam team1;

    @NotNull
    @Valid
    private final PostedGameRequestTeam team2;

And then inside

public class PostedGameRequestTeam {

@Min(0)
integer score;

@NotNull
@Size (min=1)
String color;

}

Edit:@Size applies only in String fields not custom objects

Instead of using your @Team custom validator that handles all the validation logic (which I don't think that is pretty useful in this case), you should declare your validation annotations individually inside of each PostedGameRequestTeam object.

public class PostedGameRequestTeam {

  @NotBlank(message = "The team colour value must not be null, empty or blank!")
  @TeamColour(message = "The team colour must be RED or BLUE")
  private String colour;

  @Min(value = 0, message = "The score value must be higher than 0!")
  @Max(value = 11, message = "The score value must be lesser than 11!")
  private Integer score;

  // Getters and Setters
}

Note that I used @Min and @Max annotation, that is because javax.validation.constraints.Size validates only string's length.

And now you can add the custom constraint validation of @TeamColour.

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = { StatusNotaFiscalValidator.class })
public @interface TeamColour {
    
    String message() default "The team colour must be RED or BLUE!";

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

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

}

public class TeamColourValidator implements ConstraintValidator<TeamColour, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        boolean valid = true;
        
        if (value != null) {
            if (!value.equals("BLUE") && !value.equals("RED")) {
                valid = false;
            }
        }
        
        return valid;
    }
    
}

And then, you should add @NotNull property on your parent object so you can validate the JSON body which have the team object.

@Builder
public class PostedGameRequest {
    
    @NotNull
    @Valid
    private final PostedGameRequestTeam team1;

    @NotNull
    @Valid
    private final PostedGameRequestTeam team2;

    public PostedGameRequestTeam getTeam1() {
        return team1;
    }

    public PostedGameRequestTeam getTeam2() {
        return team2;
    }

}

public @ResponseBody
    ResponseEntity<Response<Game>> saveGame(
            @Valid @RequestBody PostedGameRequest postedGameRequest
    ) {
    // ...
}

This logic handles all of cascade validation which you want to use.

Related