I'm trying to validate if one of two fields are not null in Spring Boot?
I have set that in the method class for the main object:
@NotNull(message = "Username field is required")
private String username;
@NotNull(message = "Email field is required")
private String email;
but that will require to have both fields not null. Then I went with custom validation described here https://lmonkiewicz.com/programming/get-noticed-2017/spring-boot-rest-request-validation/ but I wasn't able to get that example to work. I have to stuck on
User class declaration:
@CombinedNotNull(fields = {"username","email"})
public class User implements {
    private long id = 0L;
    @NotNull(message = "First name field is required")
    private String firstName;
    @NotNull(message = "Last name field is required")
    private String lastName;
    private String username;
    private String email;
    @NotNull(message = "Status field is required")
    private String status;
    ...all methods here...
    ...setters and getters...
}
CombibnedNotNull class:
@Documented
@Retention(RUNTIME)
@Target({ TYPE, ANNOTATION_TYPE })
@Constraint(validatedBy = userValidator.class)
public @interface CombinedNotNull {
        String message() default "username or email is required";
        Class<?>[] groups() default { };
        Class<? extends Payload>[] payload() default { };
}
userValidator class:
@Component
public class userValidator implements ConstraintValidator<CombinedNotNull, User> {
    @Override
    public void initialize(final CombinedNotNull combinedNotNull) {
        fields = combinedNotNull.fields();
    }
    @Override
    public boolean isValid(final User user, final ConstraintValidatorContext context) {
        final BeanWrapperImpl beanWrapper = new BeanWrapperImpl(user);
        for (final String f : fields) {
            final Object fieldValue = beanWrapper.getPropertyValue(f);
            if (fieldValue == null) {
                return false;
            }
        }
        return true;
    }
}
Is there any other way to get this done or should I go with the "complex" example from that page?
I'm assuming username OR email must not be null. Not XOR.
Add this getter in User class:
@AssertTrue(message = "username or email is required")
private boolean isUsernameOrEmailExists() {
    return username != null || email != null;
}
In my experience, the method name must follow the getter name convention otherwise this won't work. For examples, getFoo or isBar.
This has a small problem: the field name from this validation error would be usernameOrEmailExists, only 1 error field. If that is not a concern, this might help.
But If you want to have username and email fields when errors occur, you can use this workaround:
public String getUsername() {
    return username;
}
public String getEmail() {
    return email;
}
@AssertTrue(message = "username or email is required")
private boolean isUsername() {
    return isUsernameOrEmailExists();
}
@AssertTrue(message = "username or email is required")
private boolean isEmail() {
    return isUsernameOrEmailExists();
}
private boolean isUsernameOrEmailExists() {
    return username != null || email != null;
}
get... methods are just simple getters for general use, and is...  are for validation. This will emit 2 validation errors with username and email fields.
I'll try to implement it for you (even if I'm without an IDE).
Inside ConstraintValidator#initialize you can get a hold of the configured fields' names which cannot be null.
@Override
public void initialize(final CombinedNotNull combinedNotNull) {
    fields = combinedNotNull.fields();
}
Inside ConstraintValidator#isValid you can use those fields' names to check the Object fields.
@Override
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
    final BeanWrapperImpl beanWrapper = new BeanWrapperImpl(value);
    
    for (final String f : fields) {
       final Object fieldValue = beanWrapper.getPropertyValue(f);
       
       if (fieldValue == null) {
          return false;
       }
    }
    return true;
}
Annotation:
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Constraint(validatedBy = CombinedNotNullValidator.class)
public @interface CombinedNotNull {
    String message() default "username or email is required";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    /**
     * Fields to validate against null.
     */
    String[] fields() default {};
}
The annotation could be applied as
@CombinedNotNull(fields = {
      "fieldName1",
      "fieldName2"
})
public class MyClassToValidate { ... }
To learn how to create a Class-level constraint annotation, refer always to the official documentation. Docs
If you want to validate that exactly one is set and the others are null:
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Constraint(validatedBy = OneNotNullValidator.class)
public @interface OneNotNull {
    String message() default "Exactly one of the fields must be set and the other must be null";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    /**
     * Fields to validate against null.
     */
    String[] fields() default {};
}
import java.util.Arrays;
import java.util.Objects;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.stereotype.Component;
@Component
public class OneNotNullValidator implements ConstraintValidator<OneNotNull, Object> {
    private String[] fields;
    @Override
    public void initialize(final OneNotNull combinedNotNull) {
        fields = combinedNotNull.fields();
    }
    @Override
    public boolean isValid(final Object obj, final ConstraintValidatorContext context) {
        final BeanWrapperImpl beanWrapper = new BeanWrapperImpl(obj);
        return Arrays.stream(fields)
                .map(beanWrapper::getPropertyValue)
                .filter(Objects::isNull)
                .count()
                == 1;
    }
}
@OneNotNull(
    fields = {"username","email"},
    message="Either username or email must be set"
)
public class User {
    private String username;
    private String email;
   // ...
}
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With