Hello,
After upgrading our project to spring boot 2.5.0 from 2.4.6 we are experiencing failing tests that perform assertions on interpolated constraint violation messages using expression language. The problem only seems to occur when building a constraint violation on the context inside a validator, using the default constraint message template poses no problem.
This can be reproduced by the following code:
pom.xml
```<?xml version="1.0" encoding="UTF-8"?>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
Customer class:
import lombok.Data;
@MyConstraint @Data public class Customer {
private String username;
}
Constraint:
@Documented @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = MyConstraintValidator.class) public @interface MyConstraint { String message() default "Invalid username: ${validatedValue.username}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Test:
@ContextConfiguration(classes = ValidationTestConfig.class) @ExtendWith(SpringExtension.class) public class MyConstraintValidatorTest {
@Autowired
private Validator validator;
@Test
public void testValidation() {
Customer customer = new Customer();
customer.setUsername("foo");
Set<ConstraintViolation<Customer>> actual = validator.validate(customer);
assertThat(actual)
.hasSize(1)
.element(0)
.hasFieldOrPropertyWithValue("message", "Invalid username: foo");
}
}
Validator 1 using default constraint message:
public class MyConstraintValidator implements ConstraintValidator
@Override
public boolean isValid(Customer value, ConstraintValidatorContext context) {
return false;
}
}
Validator 2 using constraint violation builder:
public class MyConstraintValidator implements ConstraintValidator
@Override
public boolean isValid(Customer value, ConstraintValidatorContext context) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("Invalid username: ${validatedValue.username}").addConstraintViolation();
return false;
}
}
Using Validator 1 the test runs successfully, using Validator 2 the test fails:
Expecting ConstraintViolationImpl{interpolatedMessage='Invalid username: ${validatedValue.username}', propertyPath=, rootBeanClass=class com.example.demo.Customer, messageTemplate='Invalid username: ${validatedValue.username}'} to have a property or a field named "message" with value "Invalid username: foo" but value was: "Invalid username: ${validatedValue.username}"
Running the test with Validator 2 and spring boot 2.4.6 is also not a problem.
I am not familiar with the inner workings of the validator context but I have noticed that the `ExpressionLanguageFeatureLevel` of the context used in `ResourceBundleMessageInterpolator` differs in both scenarios, it is `NONE` in scenario 2 and `BEAN_PROPERTIES` in scenario 1, as I understand this changes how template tokens are collected and used.
**Comment From: wilkinsona**
The change in behaviour that you have observed is due to Spring Boot 2.5 upgrading to Hibernate Validator 6.2. You can learn more about the changes in Hibernate Validator 6.2 and the expression language feature level in [this blog post](https://in.relation.to/2021/01/06/hibernate-validator-700-62-final-released/).
If you're happy that expression evaluation is safe, you can enable it in your custom validator:
```java
@Override
public boolean isValid(Customer value, ConstraintValidatorContext context) {
HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
hibernateContext.disableDefaultConstraintViolation();
hibernateContext.buildConstraintViolationWithTemplate("Invalid username: ${validatedValue.username}")
.enableExpressionLanguage(ExpressionLanguageFeatureLevel.BEAN_PROPERTIES)
.addConstraintViolation();
return false;
}
Comment From: wilkinsona
I've added a note about this to the release notes: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.5-Release-Notes#hibernate-validator-62.
Comment From: zeno-ce
Thanks for the quick reply!