How to do conditional validation in spring boot? - validation

I am developing a Spring REST application.
I have a DTO
private String name;
#
private String nationality;
private String matchType;
private List<NC_Field> ncFields = new ArrayList();
// Getters and Setters
I have 3 tables
Field Table
Name Clearance Table
NC_Fields Table

You can have a custom validator defined with whatever logic you want.
Then you can create a custom annotation for the validator and use it in your DTO just like #NotNull
public class CustomValidator implements ConstraintValidator<MyObject, String> {
.
.
.
}
--
#Constraint(validatedBy = { CustomValidator.class })
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
public #interface ContactInfo {
String message() default "Invalid value";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
refer to this link for example: https://www.baeldung.com/spring-dynamic-dto-validation

Related

Annotation for validating a parameter in a record constructor is not working when #Target is specified as RECORD_COMPONENT

The ElementType.RECORD_COMPONENT, when applied to annotation for constructor parameter of a record does not work as I would anticipate.
I have this record:
public record MyRecord (
MultipartFile document
) {}
I want to ensure that document is file type application/pdf.
So I created the annotation:
#Documented
#Target({ElementType.RECORD_COMPONENT})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = PdfValidator.class)
public #interface ValidPdf {
String message() default "MultipartFile must be application/pdf type";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
the validator:
public class PdfValidator implements ConstraintValidator<ValidPdf, MultipartFile> {
#Override
public boolean isValid(MultipartFile file, ConstraintValidatorContext context) {
if (file == null) {
return false;
}
return "application/pdf".equals(file.getContentType());
}
}
and applied it to the record:
public record MyRecord (
#ValidPdf
MultipartFile document
) {}
This however doesn't work, the validator is never called. After a few tries I managed to make it work by changing the #Target in PdfValid interface from RECORD_COMPONENT to FIELD.
#Documented
#Target({ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = PdfValidator.class)
public #interface ValidPdf {
String message() default "MultipartFile must be application/pdf type";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Now it works. But why? Isn't document in MyRecord literally a record component? Why is FIELD the correct value here?

Spring custom validator with dependencies on other fields

We are using spring custom validator for our request object used in our controller endpoint. We implemented it the same way as how its done in the link below:
https://www.baeldung.com/spring-mvc-custom-validator
The problem we are facing is, it can't work if the particular field has dependencies on other input fields as well. For example, we have the code below as the request object for our controller endpoint:
public class FundTransferRequest {
private String accountTo;
private String accountFrom;
private String amount;
#CustomValidator
private String reason;
private Metadata metadata;
}
public class Metadata {
private String channel; //e.g. mobile, web, etc.
}
Basically #CustomValidator is our custom validator class and the logic we want is, if the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required. Else, it will be required.
Is there a way to do this? I've done additional research and can't see any that handles this type of scenario.
Obviously if you need access to multiple fields in your custom validator, you have to use a class-level annotation.
The same very article you mentioned has an example of that: https://www.baeldung.com/spring-mvc-custom-validator#custom-class-level-validation
In your case it might look something like this:
#Constraint(validatedBy = CustomValidator.class)
#Target({ ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
public #interface CustomValidation {
String message() default "Reason required";
String checkedField() default "metadata.channel";
String checkedValue() default "WEB";
String requiredField() default "reason";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package com.example.demo;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/*
If the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required.
Else, it will be required.
*/
#Component
public class CustomValidator implements ConstraintValidator<CustomValidation, Object> {
private String checkedField;
private String checkedValue;
private String requiredField;
#Override
public void initialize(CustomValidation constraintAnnotation) {
this.checkedField = constraintAnnotation.checkedField();
this.checkedValue = constraintAnnotation.checkedValue();
this.requiredField = constraintAnnotation.requiredField();
}
#Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
Object checkedFieldValue = new BeanWrapperImpl(value)
.getPropertyValue(checkedField);
Object requiredFieldValue = new BeanWrapperImpl(value)
.getPropertyValue(requiredField);
return checkedFieldValue != null && checkedFieldValue.equals(checkedValue) || requiredFieldValue != null;
}
}
And the usage will be:
#CustomValidation
public class FundTransferRequest {
...
or with parameters specified:
#CustomValidation(checkedField = "metadata.channel",
checkedValue = "WEB",
requiredField = "reason",
message = "Reason required")
public class FundTransferRequest {
...

Springboot #Valid annotation with String object

So, I have the following controller method:
#RequestMapping(path = "/{application}/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public MyObject getUsers(#RequestParam("itemId") String itemId, #PathVariable("application") String application) {
return userService.get(itemId, application);
}
I would like to check if the request parameter itemId exists in the related application (in the path).
My first idea was to create a validator :
#RequestMapping(path = "/{application}/users", method = RequestMethod.GET, produces =
MediaType.APPLICATION_JSON_VALUE)
#CheckItemId
public MyObject getUsers(#RequestParam("itemId") String itemId, #PathVariable("application") String application) {
return userService.get(itemId, application);
}
CheckItemId.java :
#Target({METHOD})
#Retention(RUNTIME)
#Constraint(validatedBy = CheckItemIdValidator.class)
#Documented
public #interface CheckItemId {
String message() default "error";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
CheckItemIdValidator.java :
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class CheckItemIdValidator implements ConstraintValidator<CheckItemId, Object[]>{
#Override
public boolean isValid(Object[] arg0, ConstraintValidatorContext arg1) {
String itemId= (String) arg0[0];
String application = (String) arg0[1];
// Logic business ...
return true;
}
}
This implementation works well, I managed to get the values itemId and application in the validator. I can now do my verification.
I was wondering if there is a better way to do something like that? Since I handle an array of Object, I need to cast it to String and If I change the parameters order, I will not get the same values since I need to use arg0[0] and arg0[1].
Thank you !
You can use spring validation library. Add #Valid on controller level. Then add #NotBlank on method level as below.
getUsers(#RequestParam("itemId") #NotBlank String itemId)

How to get validate a #PathVariable before authorization in Spring MVC

I have a REST handler with an endpoint for the GET verb. Where from an identifier (ObjectID of MongoDB) I get the information of that entity.
To validate that the ObjectID is valid and avoid errors when using Spring Data Mongo. I have developed a simple validator following the guidelines of the JPA bean validation standard.
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Retention(RUNTIME)
#Constraint(validatedBy = ValidObjectIdValidator.class)
#NotNull
#Documented
public #interface ValidObjectId {
String message() default "{constraints.valid.objectid}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class ValidObjectIdValidator implements ConstraintValidator<ValidObjectId, String> {
#Override
public void initialize(ValidObjectId constraintAnnotation) {}
#Override
public boolean isValid(String id, ConstraintValidatorContext context) {
return ObjectId.isValid(id);
}
}
Then I apply the variable-level validation of the controller using the following configuration:
#Api
#RestController("RestUserController")
#Validated
#RequestMapping("/api/v1/children/")
public class ChildrenController implements ISonHAL, ICommentHAL, ISocialMediaHAL
Using #Validated annotation at controller level.
#GetMapping(path = "/{id}")
#ApiOperation(value = "GET_SON_BY_ID", nickname = "GET_SON_BY_ID", notes = "Get Son By Id",
response = SonDTO.class)
#PreAuthorize("#authorizationService.hasParentRole() && #authorizationService.isYourSon(#id)")
public ResponseEntity<APIResponse<SonDTO>> getSonById(
#Valid #ValidObjectId(message = "{son.id.notvalid}")
#ApiParam(value = "id", required = true) #PathVariable String id) throws Throwable {
logger.debug("Get User with id: " + id);
return Optional.ofNullable(sonService.getSonById(id))
.map(sonResource -> addLinksToSon(sonResource))
.map(sonResource -> ApiHelper.<SonDTO>createAndSendResponse(ChildrenResponseCode.SINGLE_USER, HttpStatus.OK, sonResource))
.orElseThrow(() -> { throw new SonNotFoundException(); });
}
Using the #Valid annotation on the #PathVariable.
The problem is that I must verify that the user is currently authenticated is the parent of the child for whom he wants to see his information. This is verified by the execution of:
#PreAuthorize("#authorizationService.hasParentRole() && #authorizationService.isYourSon(#id)")
And here the error occurs. Because I must convert the received id to an ObjectID mediate new ObjectId (id). And this may not be valid.
Is there any way to configure validation to occur before authorization?.
This is my configuration to enable security at the method level:
#EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
Thanks in advance.

Hibernate validation with custom messages in property file

Hi am using hibernate validator in jersey rest service.
Here how can we pass value to the property file message as follows
empty.check= Please enter {0}
here in {0} i need to pass the value from annotation
#EmptyCheck(message = "{empty.check}") private String userName
here in the {0} i need to pass "user name", similarly i need to reuse message
please help me out to solve this.
You can do this by altering your annotation to provide a field description and then exposing this in the validator.
First, add a description field to your annotation:
#Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = EmptyCheckValidator.class)
#Documented
public #interface EmptyCheck {
String description() default "";
String message() default "{empty.check}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Next, change your message so that it uses a named parameter; this is more readable.
empty.check= Please enter ${description}
Since you're using hibernate-validator, you can get the hibernate validator context within your validation class and add a context variable.
public class EmptyCheckValidator
implements ConstraintValidator<EmptyCheck, String> {
String description;
public final void initialize(final EmptyCheck annotation) {
this.description = annotation.description();
}
public final boolean isValid(final String value,
final ConstraintValidatorContext context) {
if(null != value && !value.isEmpty) {
return true;
}
HibernateConstraintValidatorContext ctx =
context.unwrap(HibernateConstraintValidatorContext.class);
ctx.addExpressionVariable("description", this.description);
return false;
}
}
Finally, add the description to the field:
#EmptyCheck(description = "a user name") private String userName
This should produce the following error when userName is null or empty:
Please enter a user name

Resources