Problem
By default, the JSR 303 Bean validation doesn’t work together with Spring Boot Data REST HATEOAS. Even worse we are confronted with 500 errors like
{ "timestamp": "2020-01-01T20:52:41.336+0000", "status": 500, "error": "Internal Server Error", "message": "Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction", "trace": "org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:543) at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:744) at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:712) at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:631) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:385) .... Caused by: javax.validation.ConstraintViolationException: Validation failed for classes [xx.xxx.xxxx.xxx.YourEntity] during persist time for groups [javax.validation.groups.Default, ] List of constraint violations:[ ConstraintViolationImpl{interpolatedMessage='muss zwischen 1 und 1024 liegen',
In our Solutions
Imagin in the description that we have a bean-like this and of course a Spring Data repository:
@Entity @Data public class ValidatedBean { @GeneratedValue @Id @Column(updatable = false) private Long id; @NotNull @Size(min = 2, max = 1024) private String name; } @RepositoryRestResource(path = "mybean") public interface ValidatedBeanDAO extends JpaRepository<ValidatedBean, Long> {}
Solution 1: custom validator
One way, of course, is to follow the Spring documentation and register validator classes, which intercept the save process. This is described here.
Advantages
- Bean is validated before saving it
- No transaction is started
- Maybe cleaner
Disadvantages
- Requires the registration of a validator
- Still requires handling of response
- Controller advice needed
- Validation happens twice
Solution 1.1: Throw ConstraintViolationException
The most simple way to achieve this is by using the AbstractRepositoryEventListener
and call the validator on your own.
javax.validation.Validator
.
@Service public class HateoasValidationListenerConfig extends AbstractRepositoryEventListener<Object> { @Autowired private Validator validator; @Override public void onBeforeSave(Object entity) { final Set<ConstraintViolation<Object>> result = validator.validate(entity); if (!result.isEmpty()) { throw new ConstraintViolationException(result); } } @Override protected void onBeforeCreate(@Validated Object entity) { final Set<ConstraintViolation<Object>> result = validator.validate(entity); if (!result.isEmpty()) { throw new ConstraintViolationException(result); } } }
This will produce by default the following result:
{ "timestamp": "2020-01-01T20:56:41.070+0000", "status": 500, "error": "Internal Server Error", "message": "name: muss zwischen 2 und 1024 liegen", "trace": "javax.validation.ConstraintViolationException: name: muss zwischen 2 und 1024 liegen at xx.xxx.xxx.HateoasValidationListenerConfig.onBeforeCreate(HateoasValidationListenerConfig.java:29)
Not very „Springy“ as a Spring validation result and still error code 500. Usually, a Spring validation error looks like:
{ "timestamp": "2020-01-01T20:58:07.957+0000", "status": 400, "error": "Bad Request", "errors": [ { "codes": [ "Size.ValidatedBean.name", "Size.name", "Size.java.lang.String", "Size" ], "arguments": [ { "codes": [ "validatedBean.name", "name" ], "arguments": null, "defaultMessage": "name", "code": "name" }, 1024, 2 ], "defaultMessage": "muss zwischen 2 und 1024 liegen", "objectName": "validatedBean", "field": "name", "rejectedValue": "", "bindingFailure": false, "code": "Size" } ], "message": "Validation failed for object='validatedBean'. Error count: 1",
Solution 1.2: Throw Custom Exception
The problem is, that Spring is still built around theBindingResult
since 2.0, a far older concept & classes as the ConstraintViolation
and so doesn’t provide a default exception mapper for the ConstraintViolationException
. Let’s adjust our solution and use the SpringValidatorAdapter
.
@Service public class HateoasRepositoryValidationListenerConfig extends AbstractRepositoryEventListener<Object> { @Autowired private SpringValidatorAdapter validator; @Override protected void onBeforeCreate(Object entity) { BeanPropertyBindingResult errors = new BeanPropertyBindingResult(entity, Introspector.decapitalize(entity.getClass().getSimpleName())); validator.validate(entity, errors); // throw a custom exception containing the spring validation result // which we will use later in our controller exception mapper if (errors.hasErrors()) { throw new RepositoryListenerValidationException(errors); } } @AllArgsConstructor public static class RepositoryListenerValidationException extends RuntimeException { @Getter private final BeanPropertyBindingResult result; } }
Having this in place we need an exception mapper (@ControllerAdvice
) to extract from our BeanPropertyBindingResult
the ObjectError
.
As so we have to add to our result first the attributes defined in the DefaultErrorAttributes
furthermore by default Spring uses the BindingResult
and so the FieldError
in the errors
attribute to return the validation result.
@ControllerAdvice public class HateoasJsrErrorExceptionMapper { @ExceptionHandler(RepositoryListenerValidationException.class) public ResponseEntity<Object> handleConstraintViolation(RepositoryListenerValidationException e, ServletWebRequest request) { return ResponseEntity.badRequest().body(ValidationError.builder() .message(e.getResult().getAllErrors().get(0).getDefaultMessage()) .path(request.getRequest().getRequestURI()) .errors(e.getResult().getAllErrors()) .build()); } // Simple model class which represents our DefaultErrorAttributes. // The alternative would be to use DefaultErrorAttributes and set the // error code etc. in the request @Builder @Getter @JsonPropertyOrder({"timestamp", "status", "error", "errors", "message", "path"}) @JsonAutoDetect(isGetterVisibility = Visibility.PUBLIC_ONLY, fieldVisibility = Visibility.NONE) static class ValidationError { final Instant timestamp = Instant.now(); final String message; final String path; final List<ObjectError> errors; public int getStatus() { return HttpStatus.BAD_REQUEST.value(); } public String getError() { return HttpStatus.BAD_REQUEST.getReasonPhrase(); } } }
This will produce the expected result, which is very same as the default Spring validator will produce too.
Solution 2: extract cause exception
Alternatively, we can also extract the root cause of the TransactionSystemException
, as the validation has already taken place by hibernate and a ConstraintViolationException
is already attached as root cause.
Advantages
- Reuses the validation result from hibernate, validation is executed once
- We may handle other exceptions
- Maybe anyway needed, if we want to catch other DB validation errors like unique constraints
Disadvantages
- Maybe somehow hacky?
- DB Transaction is started, not really fail fast
Implementation
@ControllerAdvice public class HateoasJsrErrorExceptionMapper { @Autowired private MessageSource messageSource; @ExceptionHandler(TransactionSystemException.class) public ResponseEntity<Object> handleConstraintViolation(TransactionSystemException e, WebRequest request) { ResponseEntity<Object> result; if (e.getRootCause() instanceof ConstraintViolationException) { ConstraintViolationException cve = (ConstraintViolationException)e.getRootCause(); // using model class from Spring ConstraintViolationExceptionMessage msg = new ConstraintViolationExceptionMessage( cve, messageSource, request.getLocale()); result = ResponseEntity.badRequest().body(msg); } else { result = null; // not handled here } return result; } }
In the most simple way, we could, of course, use the Spring ConstraintViolationExceptionMessage
, the problem with that is, that the default handler in Spring returns a list of ObjectError
, which are just different in structure again.
{ "messages": [ { "message": "muss zwischen 2 und 1024 liegen", "invalidValue": "", "entity": "xx.xx.xxx.ValidatedBean", "property": "name" } ], "cause": "Validation failed for classes [xx.xx.xxx.ValidatedBean, messageTemplate='{javax.validation.constraints.Size.message}'}\n]" }
Solution 2.1: Custom ObjectError
We have basically the same problem as in solution 1, but somehow a bit more tricky. As our source is already a list of ConstraintViolation
s. Well straight forward we could add a class which emulates the same structure and map jus the ConstraintViolation
:
// Simpler class representing the fields of the Spring ObjectError @Getter @ToString static class SimpleObjectError { String defaultMessage; String objectName; String field; Object rejectedValue; String code; public static SimpleObjectError from(ConstraintViolation<?> violation, MessageSource msgSrc, Locale locale) { SimpleObjectError result = new SimpleObjectError(); result.defaultMessage = msgSrc.getMessage(violation.getMessageTemplate(), new Object[] { violation.getLeafBean().getClass().getSimpleName(), violation.getPropertyPath().toString(), violation.getInvalidValue() }, violation.getMessage(), locale); result.objectName = Introspector.decapitalize(violation.getRootBean().getClass().getSimpleName()); result.field = String.valueOf(violation.getPropertyPath()); result.rejectedValue = violation.getInvalidValue(); result.code = violation.getMessageTemplate(); return result; } }
But even using this class would produce a result like this:
{ "timestamp": "2020-01-01T21:08:21.826429700Z", "status": 400, "error": "Bad Request", "errors": [ { "defaultMessage": "muss zwischen 2 und 1024 liegen", "objectName": "validatedBean", "field": "name", "rejectedValue": "", "code": "{javax.validation.constraints.Size.message}" } ], "message": "Validation failed for classes [org.sterl.cloudadmin.impl.identity.model.IdentityBE] during persist time for groups [javax.validation.groups.Default, ]\nList of constraint violations:[\n\tConstraintViolationImpl{interpolatedMessage='muss zwischen 2 und 1024 liegen', propertyPath=name, rootBeanClass=class org.sterl.cloudadmin.impl.identity.model.IdentityBE, messageTemplate='{javax.validation.constraints.Size.message}'}\n]", "path": "/api/identity" }
Solution 2.2: Bridge and reuse of SpringValidatorAdapter
A different approach could be to build a small bridge class which would allow us to access the protected method processConstraintViolations
in the SpringValidatorAdapter
, even if it is maybe a bit dirty:
// simple bridge to get access to the processConstraintViolations method static class SpringValidatorBridge extends SpringValidatorAdapter { public SpringValidatorBridge(Validator validator) { super(validator); } public BeanPropertyBindingResult processConstraintViolations(ConstraintViolationException cve) { // get the bean which was validated final Object bean = cve.getConstraintViolations().iterator().next().getRootBean(); final BeanPropertyBindingResult errors = new BeanPropertyBindingResult(bean, Introspector.decapitalize(bean.getClass().getSimpleName())); // execute the spring code to map ConstraintViolations to ObjectError super.processConstraintViolations(map(cve.getConstraintViolations()), errors); return errors; } } // needed as we have to map between <?> and <Object> static Set<ConstraintViolation<Object>> map(Set<ConstraintViolation<?>> input) { return input.stream().map(v -> (ConstraintViolation<Object>)v).collect(Collectors.toSet()); }
Having this bridge we can just call it to convert the ConstraintViolationException
to a proper Spring BeanPropertyBindingResult
.
@ControllerAdvice public class HateoasJsrErrorExceptionMapper { private SpringValidatorBridge validatorBridge; @Autowired public HateoasJsrErrorExceptionMapper(Validator validator) { validatorBridge = new SpringValidatorBridge(validator); } @ExceptionHandler(TransactionSystemException.class) public ResponseEntity<Object> handleConstraintViolation(TransactionSystemException e, ServletWebRequest request) { ResponseEntity<Object> result; if (e.getRootCause() instanceof ConstraintViolationException) { ConstraintViolationException cve = (ConstraintViolationException)e.getRootCause(); BeanPropertyBindingResult errors = validatorBridge.processConstraintViolations(cve); result = ResponseEntity.badRequest().body(ValidationError.builder() .message(cve.getMessage()) .path(request.getRequest().getRequestURI()) .errors(errors.getAllErrors()) .build()); } else { result = null; // not handled here } return result; }
In the end, we can again pass the list of ObjectError
s to our created ValidationError
class, like in solution 1 and we receive the expected result from it.
Which solution to use?
Well to decide this depends on the personal taste. I personally like the 1.2 solution the most because it has the advantage to fail fast and is maybe somehow cleaner. But in then I end up doing the validation twice of course. Once on my event listener and once by hibernate.
Overall it depends on the personal taste and if it is anyway needed to catch and handle DB errors too. As so we need maybe both solutions.
Links
- Git repository to this blog
- https://docs.spring.io/spring-data/rest/docs/current/reference/html/#validation
- https://docs.spring.io/spring/docs/4.1.x/spring-framework-reference/html/validation.html
- https://jira.spring.io/si/jira.issueviews:issue-html/DATAREST-593/DATAREST-593.html
- https://stackoverflow.com/questions/45070642/springboot-doesnt-handle-javax-validation-constraintviolationexception