Expression Language not propagated to iterable node

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


}

Hello, @jlambrechts

It seems you’ve encountered a nuanced issue with the expression language feature level not propagating as expected in Hibernate Validator 8.0.1.Final. The behavior you’re experiencing is not consistent with the expected functionality, where the expression language should interpolate the {validatedValue.key} correctly without the need for reflection.

The fact that you had to use reflection to set the expressionLanguageFeatureLevel to ExpressionLanguageFeatureLevel.BEAN_PROPERTIES indicates that there might be a bug or an oversight in the propagation of the expression language feature level in the atKey() method. This is not the expected behavior, as the expression language should be enabled and applied consistently across all parts of the constraint violation building process.

I would recommend reporting this issue to the Hibernate Validator project maintainers. Providing them with the detailed information you’ve shared here, including your test case, will be very helpful. They can then investigate further and determine whether this is a bug that needs to be addressed.

I hope my suggestion is helpful for you.

Best Regard,
angela683

Hello,

I concur with the AI bot above (why do people waste their time doing that?): you should probably open an issue.

This looks like a bug, but even if it’s expected behavior, this would probably need to be documented somewhere.

Thanks.

Good evening,

I’ve opened [HV-1978] - Hibernate JIRA.

Thanks @angela683 ( I better be nice to AI bots )