14

Looking for some help with Spring data rest validation regarding proper handling of validation errors:

I'm so confused with the docs regarding spring-data-rest validation here: http://docs.spring.io/spring-data/rest/docs/current/reference/html/#validation

I am trying to properly deal with validation for a POST call that tries to save a new Company entity

I got this entity:

@Entity
public class Company implements Serializable {

@Id
@GeneratedValue
private Long id;

@NotNull
private String name;

private String address;

private String city;

private String country;

private String email;

private String phoneNumber;

@OneToMany(cascade = CascadeType.ALL, mappedBy = "company")
private Set<Owner> owners = new HashSet<>();

public Company() {
    super();
}

...

and this RestResource dao

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RestResource;

import com.domain.Company;

@RestResource
public interface CompanyDao extends PagingAndSortingRepository<Company,   Long> {


}

POST Request to api/Companies:

{
  "address" : "One Microsoft Way",
  "city" : "Redmond",
  "country" : "USA",
  "email" : "[email protected]",
  "phoneNumber" : "(425) 703-6214"

}

When I issue a POST with a null name , I get the following rest response with httpcode 500

{"timestamp":1455131008472,"status":500,"error":"Internal Server Error","exception":"javax.validation.ConstraintViolationException","message":"Validation failed for classes [com.domain.Company] during persist time for groups [javax.validation.groups.Default, ]\nList of constraint violations:[\n\tConstraintViolationImpl{interpolatedMessage='may not be null', propertyPath=name, rootBeanClass=class com.domain.Company, messageTemplate='{javax.validation.constraints.NotNull.message}'}\n]","path":"/api/companies/"}

I tried creating the following bean, but it never seems to do anything:

@Component(value="beforeCreateCompanyValidator")
public class BeforeCreateCompanyValidator implements Validator{

@Override
public boolean supports(Class<?> clazz) {
    return Company.class.isAssignableFrom(clazz);
}

@Override
public void validate(Object arg0, Errors arg1) {
    System.out.println("xxxxxxxx");


}

}

and even if it did work, how would it help me in developing a better error response with a proper http code and understandable json response ?

so confused

using 1.3.2.RELEASE

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.2.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

3 Answers 3

8

@Mathias it seems the following is enough for jsr 303 annotations to be checked and for it to auto return a http code of 400 with nice messages (I dont even need BeforeCreateCompanyValidator or BeforeSaveCompanyValidator classes):

@Configuration
public class RestValidationConfiguration extends RepositoryRestConfigurerAdapter{

@Bean
@Primary
/**
 * Create a validator to use in bean validation - primary to be able to autowire without qualifier
 */
Validator validator() {
    return new LocalValidatorFactoryBean();
}


@Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
    Validator validator = validator();
    //bean validation always before save and create
    validatingListener.addValidator("beforeCreate", validator);
    validatingListener.addValidator("beforeSave", validator);
}

}

400 response:

{
    "errors": [{
        "entity": "Company",
        "message": "may not be null",
        "invalidValue": "null",
        "property": "name"
    }, {
        "entity": "Company",
        "message": "may not be null",
        "invalidValue": "null",
        "property": "address"
    }]
}
Sign up to request clarification or add additional context in comments.

5 Comments

True - it is enough if you only have annotated constraints - if you need more complex validation you would introduce a custom validator - and I just wanted to show how that would work.
Thanks for this answer this has been bugging me for almost and hour now. I would have never guessed to make a LocalValidatorFactoryBean
RepositoryRestConfigurerAdapter now should be RepositoryRestConfigurer.
@Bean LocalValidatorFactoryBean validator() { return new LocalValidatorFactoryBean(); } // don't use Validator as return type, it may increase chances of conflict with other Validator bean.
v.addValidator("beforeCreate", validator); v.addValidator("beforeSave", validator); v.addValidator("beforeLinkSave", validator); you may also need to add "beforeLinkSave".
5

I think your problem is that the bean validation is happening too late - it is done on the JPA level before persist. I found that - unlike spring mvc - spring-data-rest is not doing bean validation when a controller method is invoked. You will need some extra configuration for this.

You want spring-data-rest to validate your bean - this will give you nice error messages responses and a proper http return code.

I configured my validation in spring-data-rest like this:

@Configuration
public class MySpringDataRestValidationConfiguration extends RepositoryRestConfigurerAdapter {

    @Bean
    @Primary
    /**
     * Create a validator to use in bean validation - primary to be able to autowire without qualifier
     */
    Validator validator() {
        return new LocalValidatorFactoryBean();
    }

    @Bean
    //the bean name starting with beforeCreate will result into registering the validator before insert
    public BeforeCreateCompanyValidator beforeCreateCompanyValidator() {
        return new BeforeCreateCompanyValidator();
    }

    @Override
    public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
        Validator validator = validator();
        //bean validation always before save and create
        validatingListener.addValidator("beforeCreate", validator);
        validatingListener.addValidator("beforeSave", validator);
    }
}

When bean validation and/or my custom validator find errors I receive a 400 - bad request with a payload like this:

    Status = 400
    Error message = null
    Headers = {Content-Type=[application/hal+json]}
    Content type = application/hal+json
   Body = {
     "errors" : [ {
     "entity" : "siteWithAdminUser",
     "message" : "may not be null",
     "invalidValue" : "null",
     "property" : "adminUser"
     } ]
   }

3 Comments

super that worked a charm, btw is there anyway to automatically have jsr 303 annotations checked instead of the 'manual' stuff I am doing here: @Override public void validate(Object object, Errors errors) { Company company = (Company)object; if(StringUtils.isEmpty(company.getName())){ errors.rejectValue("name", "name.required", "Name field is missing"); } }
I think if you have hibernate-validator on the classpath you should be able to use these constraint annotations as well - e.g. org.hibernate.validator.constraints.NotEmpty
I had wondered why your solution was still working for creates as well as updates (since I had not added a BeforeSaveCompanyValidator..).. so I started to experiment... It seems I don't even need a BeforeCreateCompanyValidator or BeforeUpdateCompanyValidator because the LocalValidatorFactoryBean configuration bean handles it all. I will post the new version of your class in the original post above. It seems to work great but still a bit confused really...
4

The answers by @Mathias and @1977 is enough for regular Spring Data REST calls. However in cases when you need to write custom @RepositoryRestControllers using @RequestBody and @Valid JSR-303 annotations didn't work for me.

So, as an addition to the answer, in case of custom @RepositoryRestControllers with @RequestBody and @Valid annotation I've added the following @ControllerAdvice:

/**
 * Workaround class for making JSR-303 annotation validation work for controller method parameters.
 * Check the issue <a href="https://jira.spring.io/browse/DATAREST-593">DATAREST-593</a>
 */

    @ControllerAdvice
    public class RequestBodyValidationProcessor extends RequestBodyAdviceAdapter {

        private final Validator validator;

        public RequestBodyValidationProcessor(@Autowired @Qualifier("mvcValidator") final Validator validator) {
            this.validator = validator;
        }

        @Override
        public boolean supports(final MethodParameter methodParameter, final Type targetType, final Class<? extends
                HttpMessageConverter<?>> converterType) {
            final Annotation[] parameterAnnotations = methodParameter.getParameterAnnotations();
            for (final Annotation annotation : parameterAnnotations) {
                if (annotation.annotationType().equals(Valid.class)) {
                    return true;
                }
            }

            return false;
        }

        @Override
        public Object afterBodyRead(final Object body, final HttpInputMessage inputMessage, final MethodParameter
                parameter, final Type targetType, final Class<? extends HttpMessageConverter<?>> converterType) {
            final Object obj = super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
            final BindingResult bindingResult = new BeanPropertyBindingResult(obj, obj.getClass().getCanonicalName());
            validator.validate(obj, bindingResult);
            if (bindingResult.hasErrors()) {
                throw new RuntimeBindException(bindingResult);
            }

            return obj;
        }
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.