Search code examples
javaspring-bootvalidationspring-data-restspring-rest

How can I return validation errors in a RFC-7807 format in Spring Boot Data Rest controllers?


I have a Spring repository

@RepositoryRestResource
public interface MongoIntegrationTokenRepository extends MongoRepository<IntegrationToken, String>, CrudRepository<IntegrationToken, String> {}

I've added the validation configuration to add validation support and my entity has the validation annotations:

@Configuration
class MyRestMvcConfiguration implements RepositoryRestConfigurer {

    private final LocalValidatorFactoryBean localValidatorFactoryBean;

    @Autowired
    public MyRestMvcConfiguration(LocalValidatorFactoryBean localValidatorFactoryBean) {
        this.localValidatorFactoryBean = localValidatorFactoryBean;
    }

    @Override
    public void configureValidatingRepositoryEventListener(
            ValidatingRepositoryEventListener validatingRepositoryEventListener) {
        validatingRepositoryEventListener.addValidator("beforeCreate", localValidatorFactoryBean);
        validatingRepositoryEventListener.addValidator("beforeSave", localValidatorFactoryBean);
        validatingRepositoryEventListener.addValidator("beforeSave", localValidatorFactoryBean);
    }
}

When running the create operation, if there are any validation errors, the entity creation fails but the JSON response doesn't contain any errors details.

A POST to my endpoint with invalid data simply returns:

{
  "message": "Server Error",
  "details": [
    "Validation failed"
  ]
}

I'd like to return the validation errors in the RFC7807 format. This should be possible via Spring HATEOAS or by this popular library by Zalando https://github.com/zalando/problem-spring-web but I'm unsure how they need to be wired in or which approach should be used.


Solution

  • I found this lone example on Github. https://github.com/marciovmartins/problem-spring-web-starter-expanded/blob/aed5825c958fad93f4aaad022689958926cf3b4a/src/main/kotlin/com/github/marciovmartins/problem/spring/web/expanded/ProblemExceptionHandler.kt and rewrote it in Java. This seems to do it.

    import java.util.List;
    import java.util.stream.Collectors;
    
    import org.springframework.data.rest.core.RepositoryConstraintViolationException;
    import org.springframework.http.ResponseEntity;
    import org.springframework.validation.FieldError;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.context.request.NativeWebRequest;
    import org.zalando.problem.Problem;
    import org.zalando.problem.ThrowableProblem;
    import org.zalando.problem.spring.web.advice.ProblemHandling;
    import org.zalando.problem.violations.Violation;
    
    @ControllerAdvice
    public class ProblemControllerAdvice implements ProblemHandling {
    
        @ExceptionHandler
        public ResponseEntity<Problem> handleRepositoryConstraintViolationException(RepositoryConstraintViolationException exception, NativeWebRequest request) {
            List<Violation> violationList = exception.getErrors().getAllErrors()
                    .stream()
                    .map(objectError -> {
                        if (objectError instanceof FieldError) {
                            return new Violation(((FieldError) objectError).getField(), objectError.getDefaultMessage());
                        }
                        return new Violation(null, objectError.getDefaultMessage());
                    })
                    .collect(Collectors.toList());
    
            ThrowableProblem problem = Problem.builder()
                    .withTitle("Constraint Violation")
                    .withStatus(defaultConstraintViolationStatus())
                    .with("violations", violationList)
                    .build();
            return create(problem, request);
        }
    
    }