Spring Boot - how to validate fields that depend on each other?

Viewed 2463

Is there some way in Spring Boot that I can perform validation on properties that depend on each other's values, and have the error message be associated with the property?

I want to return the errors to the user in a nice JSON structure:

{
  "errors": {
    "name": "is required if flag is true"
  }
}

Example:

@Entity
public class MyEntity {
  private boolean nameRequiredFlag;

  // Required if "nameRequiredFlag" is set to true:
  private String name;
}

One solution that doesn't solve my problem of associating the error message with the name property is to create a validator annotation for the entity:

@ValidEntity
public class MyEntity {
  private boolean nameRequiredFlag;

  // Required if "nameRequiredFlag" is set to true:
  private String name;
}

@Constraint( validatedBy = { MyEntityValidator.class } )
@Documented
@Target( { ElementType.TYPE } )
@Retention( RetentionPolicy.RUNTIME )
public @interface ValidEntity{
  Class<?>[] groups () default {};

  String message () default "name is required if 'nameRequiredFlag' is true";

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


public class MyEntityValidator implements Validator<ValidEntity, MyEntity> {
  @Override
  public boolean isValid ( MyEntity entity, ConstraintValidatorContext context ) {
    if ( !entity.nameRequiredFlag ) return true;
    
    return !StringUtils.isBlank( entity.getName() );
  }
}

This is laughably cumbersome and doesn't solve my problem. Isn't there any way I can do this with the framework validation?

Edit: This is for a JSON API, and the consumer really needs to be able to associate the error message to a best guess at which field has an issue. It is not helpful to send the consumer an error message for the whole object, or a computed property.

2 Answers

Solution given by @EvicKhaosKat is one way of doing it. However, when there are too many fields dependent on each other in a complicated way, your class becomes full of annotations and I personally struggle a lot relating them.

A simpler approach is to create a method(s) in your pojo which does the cross field validations and returns a boolean. On the top of this method annotate it with @AssertTrue(message = "your message"). It will solve your problem in a cleaner fashion.

public class SampleClass {

    private String duration;
    private String week;
    private String month;

    @AssertTrue(message = "Duration and time attributes are not properly populated")
    public boolean isDurationCorrect() {
        if (this.duration.equalsIgnoreCase("month")) {
            if (Arrays.asList("jan", "feb", "mar").contains(month))
                return true;
        }

        if (this.duration.equalsIgnoreCase("week")) {
            if (Arrays.asList("1-7", "8-15", "16-24", "25-31").contains(week))
                return true;
        }

        return false;
    }
}

Note: I have not tested this code but have used this approach in multiple places and it works.

Possible reason is that name validation operates on not-yet-fully constructed object, so nameRequiredFlag is not filled yet.

As an option there is a @GroupSequence annotation, which allows to group and perform validations in an order you specify. For example it is possible to add to MyEntity annotations:

@ValidEntity(groups = DependentValidations.class)
@GroupSequence({MyEntity.class, DependentValidations.class})

So all the other validation annotations on MyEntity class gonna be performed first, and after that DependentValidations group, which consists of ValidEntity. Thus ValidEntity will be called on fully created object, and the last in order.

(DependentValidations.class - just an empty interface created somewhere nearby, like any other marker interface)

https://www.baeldung.com/javax-validation-groups will possibly describe that in much more details.

p.s. answer provided by @Innovationchef will possibly suit the case more :)

Related