Hi,
After migrating from Hibernate Validator 5.1.3.Final to 8.0.1.Final I notice a
difference in behaviour when defining a violation on a map key with a message that
contains the expression {validatedValue.key}
.
In 5.1.3 we have the following:
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("'${validatedValue.key}' is not a valid value")
.addBeanNode().inIterable().atKey(key)
.addConstraintViolation();
For 8.0.1.Final I change this to the following which results in a non-interpolated message:
final HibernateConstraintValidatorContext unwrap = context.unwrap(HibernateConstraintValidatorContext.class);
unwrap.disableDefaultConstraintViolation();
unwrap.buildConstraintViolationWithTemplate("'${validatedValue.key}' is not a valid value")
.enableExpressionLanguage(ExpressionLanguageFeatureLevel.BEAN_PROPERTIES)
.addBeanNode().inIterable().atKey(key)
.addConstraintViolation();
Debugging shows that the method atKey()
does not propagate the expressionLanguageFeatureLevel
.
The same thing happens if you use atIndex
.
Also via .addPropertyNode(null).inIterable().atKey(key)
and
.addPropertyNode(null).inIterable().atIndex()
I assume this is a bug as I can’t find an example in the Hibernate Validator
project that shows the same but correct behaviour.
As a test, I used reflection to set field expressionLanguageFeatureLevel
to
ExpressionLanguageFeatureLevel.BEAN_PROPERTIES
which works as expected.
Is this a bug or expected behaviour and is there another way to solve this?
Thanks in advance,
Joris
Below a complete test example using Junit 5 including the reflection hack.
import jakarta.validation.*;
import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext;
import org.hibernate.validator.messageinterpolation.ExpressionLanguageFeatureLevel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.annotation.*;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ExpressionLanguagePropagationTest {
protected Validator validator;
@BeforeEach
void setup() {
try (ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
.buildValidatorFactory()) {
validator = factory.getValidator();
}
}
@Test
void shouldUseCustomInterpolatedMessageAndMapPathWithReflection() {
final MyObject myObject = new MyObject();
myObject.setMap(Map.of("foo", "bar"));
final Set<ConstraintViolation<MyObject>> violations = validator.validate(myObject);
assertEquals(1, violations.size());
final ConstraintViolation<MyObject> violation = violations.iterator().next();
assertEquals("'bar' is not a valid value for foo", violation.getMessage());
assertEquals("map[foo]", violation.getPropertyPath().toString());
}
static class MyObject {
@MapValidator.ValidMap
Map<String, String> map;
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
}
public static class MapValidator implements ConstraintValidator<MapValidator.ValidMap, Map<String, String>> {
@Override
public boolean isValid(Map<String, String> map, ConstraintValidatorContext context) {
if (map.containsKey("foo") && map.get("foo").equals("bar")) {
final HibernateConstraintValidatorContext unwrap = context.unwrap(HibernateConstraintValidatorContext.class);
unwrap.disableDefaultConstraintViolation();
final ConstraintValidatorContext.ConstraintViolationBuilder.LeafNodeBuilderDefinedContext leafNodeBuilderDefinedContext = unwrap.buildConstraintViolationWithTemplate("'${validatedValue.foo}' is not a valid value for foo")
.enableExpressionLanguage(ExpressionLanguageFeatureLevel.BEAN_PROPERTIES)
.addBeanNode().inIterable().atKey("foo");
final Field expressionLanguageFeatureLevelField;
try {
final Class<? extends ConstraintValidatorContext.ConstraintViolationBuilder.LeafNodeBuilderDefinedContext> aClass = leafNodeBuilderDefinedContext.getClass();
final Class<?> superclass = aClass.getSuperclass();
expressionLanguageFeatureLevelField = superclass.getDeclaredField("expressionLanguageFeatureLevel");
expressionLanguageFeatureLevelField.setAccessible(true);
expressionLanguageFeatureLevelField.set(leafNodeBuilderDefinedContext, ExpressionLanguageFeatureLevel.BEAN_PROPERTIES);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
leafNodeBuilderDefinedContext
.addConstraintViolation();
return false;
}
return true;
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Constraint(validatedBy = MapValidator.class)
@interface ValidMap {
String message() default "The map is not valid";
Class<?>[] groups() default {};
Class<?>[] payload() default {};
}
}
}