I can't figure out how to resolve the following use case in Spring Boot. Indeed, I have a Spring Boot Rest Api (eg: user-api) with the following controller method with a custom validator for a parameter :
#PostMapping
public User createUser(#ValidZipCode #RequestBody #Valid User user){
return userService.saveUser(user);
}
The User Class is defined in an external dependency (eg: user-model). It has the following fields :
public class User {
#NotNull
private String firstName;
#NotNull
private String lastName;
private String zipCode;
// getters, setters ..
}
In, user-api I created the following custom annotation :
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = ZipCodeValidator.class)
public #interface ValidZipCode {
String message() default "Must be a valid zipCode. Found: ${validatedValue}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And so the ZipCodeValidator implementation :
public class ZipCodeValidator implements ConstraintValidator<ValidZipCode, User> {
private ZipCodeService zipCodeService;
#Override
public void initialize(ValidZipCode constraintAnnotation) { }
#Override
public boolean isValid(User user, ConstraintValidatorContext constraintValidatorContext) {
return !Objects.isNull(user.getZipCode()) ?
zipCodeService.isValidZipCode(user.getZipCode()) :
false;
}
NB: zipCodeService.isValidZipCode() is a simple boolean method.
The problem is that when I call the endpoint it never access the #ValidZipCode annotation. Is there any bean configuration to set up to make it works ?
Thks for your help ;)
UPDATE
Thanks to #cassiomolin for his answer. Indeed, when I annotate the controller class with #Validated It works :D
I Hope this post will help other devs ;)
Ensure that your controller class is annotated with #Validated.
See the following quote from the documentation:
To be eligible for Spring-driven method validation, all target classes need to be annotated with Spring’s #Validated annotation [...]
Related
I'm using Spring Boot 2.4. I have the following controller with a method that accepts a MultipartFile object.
#RestController
public class MyController extends AbstractController
...
#Override
public ResponseEntity<ResponseData> add(
...
#Parameter(description = "file detail") #Validated #RequestPart("myFile")
MultipartFile myFile,
...
) {
I would like to validate that this MultipartFile contains the data that I want (e.g. is of a particular mime type). So I have written the below validator ...
#Documented
#Constraint(validatedBy = MultipartFileValidator.class)
#Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface MultipartFileConstraint {
String message() default "Incorrect file type.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and its implementation class ...
public class MultipartFileValidator
implements ConstraintValidator<MultipartFileConstraint, MultipartFile> {
#Override
public void initialize(final MultipartFileConstraint constraintAnnotation) {
log.info("\n\n\n\nconstructor called\n\n\n\n");
}
#Override
public boolean isValid(
MultipartFile file, ConstraintValidatorContext constraintValidatorContext) {
log.info("Validating file");
...
}
}
However, when I invoke my endpoint, I don't see that my validator is called (for one, the log statement is never printed nor breakpoints hit). What else do I need to do to register my validator for this MultipartFile param?
As per the Spring Documentation:
Can also be used with method level validation, indicating that a
specific class is supposed to be validated at the method level (acting
as a pointcut for the corresponding validation interceptor), but also
optionally specifying the validation groups for method-level
validation in the annotated class. Applying this annotation at the
method level allows for overriding the validation groups for a
specific method but does not serve as a pointcut; a class-level
annotation is nevertheless necessary to trigger method validation for
a specific bean to begin with. Can also be used as a meta-annotation
on a custom stereotype annotation or a custom group-specific validated
annotation.
So, here we have to keep in mind what are the placement of #Validated and validator annotation.
Code:
Controller class : #Validated added at class level and #ValidFile (Custom validator annotation) in the method
#RestController
#Validated
#Slf4j
public class MyController {
#RequestMapping("/add")
public ResponseEntity<ResponseData> add(#ValidFile #RequestParam("file") MultipartFile file) {
log.info("File Validated");
return ResponseEntity.status(HttpStatus.OK).body(new ResponseData("Valid file received"));
}
}
Validator Annotation
#Documented
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {FileValidator.class})
public #interface ValidFile {
Class<? extends Payload> [] payload() default{};
Class<?>[] groups() default {};
String message() default "Only pdf,xml,jpeg,jpg files are allowed";
}
Validator class
#Slf4j
public class FileValidator implements ConstraintValidator<ValidFile, MultipartFile> {
#Override
public void initialize(ValidFile validFile) {
log.info("File validator initialized!!");
}
#Override
public boolean isValid(MultipartFile multipartFile,
ConstraintValidatorContext constraintValidatorContext) {
log.info("Validating file");
String contentType = multipartFile.getContentType();
assert contentType != null;
return isSupportedContentType(contentType);
}
private boolean isSupportedContentType(String contentType) {
return contentType.equals("application/pdf")
|| contentType.equals("text/xml")
|| contentType.equals("image/jpg")
|| contentType.equals("image/jpeg");
}
}
Output :
Success:
{
"message": "Valid file received"
}
Exception handler
#ExceptionHandler(ConstraintViolationException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
return new ResponseEntity<>("Validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST);
}
Failure:
Validation error: Only pdf,xml,jpeg,jpg files are allowed
Below is a small example. I hope it will help.
#Component
public class MultipartFileValidator implements Validator {
#Override
public boolean supports(Class < ? > clazz) {
return MultipartFile.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
MultipartFile multipartFile = (MultipartFile) target;
if (multipartFile.isEmpty()) {
// Add an error message to the errors list
errors.rejectValue("file", "required.file");
}
}
}
My purpose is to use a specific ConstraintValidator in 2 scenarios below
use annotations on POJO to validate the whole object (the popular way)
validate a single value with specific validator's isValid function (for some configurable dynamic validation request)
The validator must support services injection, so I must get it from spring but not create a new validator instance manually.
followed my test codes:
annotation
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = { IdNumberValidator.class })
public #interface IdNumber {
String message() default "id number is not available";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
validator
public class IdNumberValidator implements ConstraintValidator<IdNumber, String>, BeanNameAware {
#Autowired
private IUserService usrService;
private String bn;
#Override
public boolean isValid(String value, ConstraintValidatorContext context){
System.out.println("my name is:" + bn);
return usrService.getUserByAccount(value).isPresent();
}
#Override
public void setBeanName(String name) {
this.bn = name;
}
}
pojo
#Getter
#Setter
public class TestPOJO {
private long id;
#IdNumber
private String idn;
}
service
#Service
public class TestValidatorService {
#Autowired
private Validator validator;
#Autowired
private ApplicationContext context;
public void validatePojo(TestPOJO pojo){
BeanPropertyBindingResult e = new BeanPropertyBindingResult(pojo, "TestPOJO");
validator.validate(pojo, e);
if(e.hasErrors()){
for(ObjectError oe : e.getAllErrors()){
System.out.println(oe.toString());
}
}
}
public void validatePojoByDynamicValidator(TestPOJO pojo){
IdNumberValidator validator = context.getBean("com.test.IdNumberValidator", IdNumberValidator.class); // got the name via BeanNameAware but seems not working
System.out.println(validator.isValid(pojo.getIdn(), null));
}
}
In the test case for service, function "validatePojo" passed but "validatePojoByDynamicValidator" did not.
Any solution for this problem? Thanks!
I am trying to validate a field of an Entity using custom ConstraintValidator implementation in a multi-tenancy setup using spring hibernate.
How do we make the custom validator tenant aware? The entity manager and other autowired beans in the validator are always null.
Entity Class:
#Entity
#Table(name = "autoupdate_json")
public class AutoupdateJson {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(name="autoupdate_id")
private String autoupdateId;
#Column(name="deployment_json", length=10000)
#ValidateAutodeploymentConfig
#Convert(converter=JpaConverterJson.class)
private ApplyInputJson deploymentJson;
}
`
Validation Annotation:
#Documented
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {AutodeploymentConfigValidator.class})
public #interface ValidateAutodeploymentConfig {
String message() default "One or More of the Devices already have perpetual mode settings enabled. Please cancel the existing "
+ " deployment config to add a new one, Updates are not supported. ";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator Implementation:
public class AutodeploymentConfigValidator
implements ConstraintValidator<ValidateAutodeploymentConfig, ApplyInputJson> {
#PersistenceContext
EntityManager entityManager;
#Autowired
DeviceRepository deviceRepository;
#Override
public boolean isValid(ApplyInputJson value, ConstraintValidatorContext context) {
List<String> deviceSerialNumbers = Arrays.asList(value.getDevices().getSerial());
System.out.println("entityManager: " + entityManager);
System.out.println("repo bean: " + deviceRepository);
}
Both the sysouts are null.
The entity manager and device repository are null because AutodeploymentConfigValidator is not coming from spring context.
It is created by default constraint validation factory used by hibernate which using no arg constructor.
Try to add HibernatePropertiesCustomizer to tell hibernate which validation factory to use. Something like:
#Component
public class HibernateCustomizer implements HibernatePropertiesCustomizer {
private final ValidatorFactory validatorFactory;
public HibernateCustomizer(ValidatorFactory validatorFactory) {
this.validatorFactory = validatorFactory;
}
public void customize(Map<String, Object> hibernateProperties) {
hibernateProperties.put("javax.persistence.validation.factory", validatorFactory);
}
}
With this you should be able to use entity manager in your constraint validator.
Request Param validated with custom annotation #ValidNumber which is created by using the below code.
#Constraint(validatedBy=NumberValidater.class)
#Target(Field, Parameter,..)
#Retention(RUNTIME)
#Documented
public #interface ValidNumber {
String message() default
"{javax.validation.constraints.ValidNumber.message}"
Class<?>[] groups() default {};
Class<? extends PayLoad>[] payload() default {};
}
NumberValidator Class
public NumberValidator implements <ConstraintValidator, Integer>{
public void intialize(ValidNumber constraintAnnotation){
}
public boolean isValid(Integer number, ConstraintValidatorContext context){
return(number !=null && number !=0)
}
Configuration Class:
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
Controller Class is:
#RestController
#Validated
public StudentController{
#RequestMapping(value="rollNumber", method=GET)
public Student getStudentHallticket(#ValidNumber (message= "Invalid RollNumber)
#RequestParam(vallue = "rollNumber") Integer rollNumber{
return service.getHallTicket();
}
Java Class executed perfectly and validated the input value, but the test class is not correctly validating the input param.
Test Case:
class StudentSpec extends Specification{
#Autowired
private StudentController studentController;
#Autowired
private LocalValidatorFoctoryBean validator
private MockMvc mockmvc
def setup(){
mockmvc.MockMvcBuilders.standloneSetup(studentController).setValidator(validator).build()
}
def getStudentHallTicket(){
given:
def rollNumber = ""
when:
def response = mockmvc.perform(get("/rollNumber/").param("rollNumber", rollNumber)
then:
response.andExpect(status().isBadRequest())
}
}
When executed it is giving `isOk()` instead of `isBadRequest()` status that means request parameter not validating when it is null.
My question is, how do I make sure the custom annotation is available for testing class?
Custom validator excludes hibernate.validator in spring mvc.
#RestController
public void foo(#Valid Bar bar){
}
now annotating class like this :
#FooAnnotation
public class Bar{
#NotNull
private String name;
private List<Foo> foos;
public List<Foo> getFoos(){
return foos;
}
}
And finally validator
#Documented
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy =FooConstraint.class)
public #interface FooAnnotation{
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
So using regular spring-boot this setup works just fine. Having to re-work one legacy rest-api and not being able to upgrade to latest version of spring or hibernate validator i am kinda stuck.
removing #FooAnnotation everything works fine #NotNull works as expected, but with #FooAnnotation my custom validation logic is working but #NotNull is not.
Ideas?
EDIT :
EDIT2 : added validation logic
EDIT3 : versions
public class FooConstraint implements ConstraintValidator<FooAnnotation,Bar> {
#Override
public void initialize(FooAnnotation ff) {
}
#Override
public boolean isValid(Bar resource, ConstraintValidatorContext ctx) {
return (resource.getFoos().size()<30);
}
}
hibernate-validator - 4.3.1.Final
spring-webmvc - 4.2.4.RELEASE