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:
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 = 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 {};
}
Validator
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;
}
}
Usage
#OneNotNull(
fields = {"username","email"},
message="Either username or email must be set"
)
public class User {
private String username;
private String email;
// ...
}
Related
I have created a custom validation annotation to check if the password and confirm password are equal. All the things work fine, but the only issue is I cannot get the field name anymore since it is a class-level constraint instead of field. I only can use the getAllErrors() to get the default message. (Refer to this image)
AddUserDTO
#Data
#NoArgsConstructor
#AllArgsConstructor
#ConfirmPassword
public class AddUsersEntityDTO {
//...blablabla
#NotBlank(message ="Password is required")
#Size(min = 6, message = "Password must be more than 5 characters")
public String password;
#NotBlank(message ="Confirm Password is required")
public String confirmPassword;
}
Validator
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = ConfirmPasswordValidation.class)
public #interface ConfirmPassword {
String message() default "Confirm password is not same with the password";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
public class ConfirmPasswordValidation implements ConstraintValidator<ConfirmPassword, AddUsersEntityDTO> {
#Override
public void initialize(ConfirmPassword constraintAnnotation) {
}
#Override
public boolean isValid(AddUsersEntityDTO addUsersEntityDTO, ConstraintValidatorContext constraintValidatorContext) {
return addUsersEntityDTO.getConfirmPassword().equals(addUsersEntityDTO.getPassword());
}
}
I have a class Wrapper that contains two arraylist and another properties :
public class Wrapper<T, E> {
private List<T> toInsert = new ArrayList<>();
private List<T> toUpdate = new ArrayList<>();
private Set<E> toDelete = new HashSet<>();
// getter setter ....
}
I have a Class Dto :
public class UserDto {
private long id;
private LocalDateTime createdDate;
#NotBlank
private String registrationNumber;
#NotBlank
private String firstName;
#NotBlank
private String lastName;
#NotBlank
private String email;
#NotBlank
}
I create a validator :
#Target({ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {CustomerLocationValidator.class})
#Documented
public #interface ValidCustomerLocation {
String message() default "Invalid List userDto";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
The implementation of validator annotation :
public class CustomerLocationValidator implements ConstraintValidator<ValidCustomerLocation, Wrapper<UserDto, String>> {
#Autowired
Validator validator;
#Override
public boolean isValid(Wrapper<UserDto, String> value, ConstraintValidatorContext constraintValidatorContext) {
boolean isValid = true;
final Set<ConstraintViolation<UserDto>> constraintViolations = new HashSet();
value.getToInsert().forEach(userDto -> constraintViolations.addAll( validator.validate(userDto)) );
if (!CollectionUtils.isEmpty(constraintViolations)) {
constraintValidatorContext.disableDefaultConstraintViolation();
for (ConstraintViolation<UserDto> violation : constraintViolations) {
constraintValidatorContext.buildConstraintViolationWithTemplate(violation.getMessageTemplate()).addConstraintViolation();
}
isValid = false;
}
return isValid;
}
}
The method in controller :
#PostMapping
public ResponseEntity<Void> save(
#RequestBody #ValidCustomerLocation Wrapper<UserDto, String> itemsUsers) {}
In this controlller I try to send invalid data but the annotation ValidCustomerLocation didn't catch the error , I try to use #Valid but i have the same problem
I'm trying to accomplish the use of a validator into the controller. The two fields origin and destination should be of three capital letters as IATA Code. But it acts without filter, and any request is accepted.
Set of validator interface:
#Documented
#Constraint(validatedBy = IATACodeValidator.class)
#Target({ElementType.METHOD, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface IATACodeConstraint {
String message() default "Invalid IATA code";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Class of Validator:
public class IATACodeValidator implements ConstraintValidator<IATACodeConstraint, String> {
#Override
public void initialize(IATACodeConstraint iataCode) {
}
#Override
public boolean isValid(String codeField, ConstraintValidatorContext context) {
return codeField != null && codeField.matches("^[A-Z]{3}+$")
&& (codeField.length() == 3);
}
Basic class:
public class CrazyAirRequest {
#IATACodeConstraint
private String origin;
#IATACodeConstraint
private String destination;
private String departureDate;
private String returnDate;
private int passengerCount;
// getters & setters
Controller:
#RestController
#RequestMapping("flights")
#Validated
public class BusyFlightsController {
CrazyAirDatabase crazyAirService;
ToughJetDatabase toughJetService;
#Autowired
public BusyFlightsController(CrazyAirDatabase crazyAirService, ToughJetDatabase toughJetService) {
this.crazyAirService = new CrazyAirDatabase();
this.toughJetService = new ToughJetDatabase();
}
#RequestMapping(value = "/crazy-air-response", method = RequestMethod.GET)
public List<CrazyAirResponse> getCrazyAirResponse(
#RequestParam("origin") String origin,
#RequestParam("destination") String destination,
#RequestParam("departureDate") String departureDate,
#RequestParam("returnDate") String returnDate,
#RequestParam("passengerCount") int passengerCount
) {
CrazyAirRequest crazyAirRequest = new CrazyAirRequest(origin, destination, departureDate, returnDate,
passengerCount);
return crazyAirService.getCrazyAirResponse(crazyAirRequest);
}
You can directly use #Pattern annotation without creating a custom validator for two fields as shown below :
#Pattern(regexp ="^[A-Z]{3}" message ="Invalid IATA code")
private String origin;
#Pattern(regexp ="^[A-Z]{3}" message ="Invalid IATA code")
private String destination;
#virendra chauhan: thank you for your inspiration.
I solved so:
#RequestMapping(value = "/crazy-air-response", method = RequestMethod.GET)
public List<CrazyAirResponse> getCrazyAirResponse(
#RequestParam("origin") #Pattern(regexp = "^[A-Z]{3}", message = "Invalid IATA code")
String origin,
#RequestParam("destination") #Pattern(regexp = "^[A-Z]{3}", message = "Invalid IATA code")
String destination,
#RequestParam("departureDate") String departureDate,
#RequestParam("returnDate") String returnDate,
#RequestParam("passengerCount") int passengerCount
) {
CrazyAirRequest crazyAirRequest = new CrazyAirRequest(origin, destination, departureDate, returnDate,
passengerCount);
return crazyAirService.getCrazyAirResponse(crazyAirRequest);
}
I have a dto like this
#FieldMatch(first = "email", second = "emailConfirm", message = "E-posta adresleri eslesmeli")
public class EmailDTO {
private String email;
private String emailConfirm;
this is validator
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
private String firstFieldName;
private String secondFieldName;
private String message;
#Override
public void initialize(final FieldMatch constraintAnnotation) {
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
message = constraintAnnotation.message();
}
#Override
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
boolean valid = true;
try
{
final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
final Object secondObj = BeanUtils.getProperty(value, secondFieldName);
valid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
}
catch (final Exception ignore)
{
// ignore
}
if (!valid){
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(firstFieldName)
.addConstraintViolation()
.disableDefaultConstraintViolation();
}
return valid;
}
}
this is interface
arget({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = FieldMatchValidator.class)
#Documented
public #interface FieldMatch {
String message() default "The fields must match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String first();
String second();
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Documented
#interface List
{
FieldMatch[] value();
}
}
but i also want to change the class to this
#FieldMatch(first = "email", second = "emailConfirm", message = "E-posta adresleri eslesmeli")
public class EmailDTO {
#Max(value = 150, message = "E-posta karakter sayisi fazla!")
#Email(message = "Email should be valid")
#NotNull(message = "Name cannot be null")
#NotEmpty(message = "Name cannot be null")
private String email;
#Max(value = 150, message = "E-posta karakter sayisi fazla!")
#Email(message = "Email should be valid")
#NotNull(message = "Name cannot be null")
#NotEmpty(message = "Name cannot be null")
private String emailConfirm;
Should I use another generic constraint or a lot of annotations like shown above? I also have other entities like password, etc and those entities will also have same validations.
Some fields can be nullable, so not every field has to be checked by null constraint. I want to do a very generic validation. I will send to front end (thymeleaf), so I need to see which constraint is violated.
Email has #email annotation that wont be in the password validation. Others are common. like
notnull, notempty, matching, notblank
I was not able to find good examples. I found below posts on the topic, but I could not find an example of custom + for example #email validation together.
spring-custom-annotation-validation-with-multiple-field
how-can-i-validate-two-or-more-fields-in-combination
spring-mvc-custom-validator post shows validation for different fields.
The following code is my annotation class
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Documented
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
public #interface Search {
public String name();
public String value();
}
Following class is a normal class
public class MyClass{
#Search(name = "first_name", value = "srivas")
private String first_name;
#Search(name = "last_name", value = "sam")
private String last_name;
public String getFirst_name() {
return first_name;
}
public void setFirst_name(String customername) {
this.customername = customername;
}
public String getLast_name() {
return last_name;
}
public void setLast_name(String last_name) {
this.last_name= last_name;
}
}
Here I am going to read variable values
public class MyService{
public void getvalues(){
MyClass myvals = new MyClass();
myvals.setFirst_name("Vikram");
myvals.setLast_name("Kumar");
for(Field f: MyClass.class.getDeclaredFields()){
MyClass searchfields = f.getAnnotation(MyClass.class);
if (searchfields != null)
System.out.println(searchfields.name()+" = "+searchfields .value());
}
}
}
I am getting the following output
first_name = srivas,
last_name = sam,
but I am expecting.
first_name = vikram,
last_name = Kumar,
Is it possible to read please help me if any posibility is there
No
Also it really makes no sense to do what you are trying to do.
The Annotation is used to perform an action at compile time, it's not a store for variable data.
You could use reflection to find an annotation and work out which variable it's related to, but it's a lot of effort when you could just use a getter to get the value of the variable without all of that hassle.