Spring Boot Hateoas JSR Validation

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> {}

Link to the full source code.

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.

Note: Import in this example the 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 ConstraintViolations. 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 ObjectErrors 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

Paul Sterl has written 22 articles

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>