Home > Software design >  Upgrading from SpringBoot 2.6.6 to 2.6.7 changed behaviour with validation ( javax.validation.Constr
Upgrading from SpringBoot 2.6.6 to 2.6.7 changed behaviour with validation ( javax.validation.Constr

Time:05-01

Just recently upgraded from SpringBoot 2.6.6 to 2.6.7, but despite the GAV for hibernate-validator has not changed ( both releases use org.hibernate.validator:hibernate-validator:6.2.3.Final and jakarta.validation:jakarta.validation-api:2.0.2 ), I noticed a change in behaviour using the validation API.

I was able to reduce it to simple test case to show the difference between 2.6.6 and 2.6.7.

NOTE: I understand the reason why it fails and know the fix, but what I don't understand is why it only started to fail with SpringBoot 2.6.7 when Hibernate validator artifact did not change between releases.

To demonstrate, here are the code and test cases:

  1. First the POM file ( using SpringBoot 2.6.6 ):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.6</version>
        <relativePath/>
    </parent>
    <groupId>org.example</groupId>
    <artifactId>validator</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  1. Then the java interface and classes :
package org.example.demo;

public interface WithNameOnCard {

    String getNameOnCard();
    void setNameOnCard(String nameOnCard);

}
package org.example.demo;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Getter
@Setter
public class MembershipCard implements WithNameOnCard {

    @NotNull
    @NotBlank
    private String nameOnCard;

    @NotNull
    private String membershipNumber;

}
package org.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}
  1. Finally, the test case :
package org.example.demo;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

public class ValidatorTest {

    public static Validator validator;

    @BeforeAll
    public static void setUp() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    public void whenPropertiesEmptyThenFailValidation() {
        MembershipCard membershipCard = new MembershipCard();

        Set<ConstraintViolation<MembershipCard>> violations = validator.validate(membershipCard);
        Assertions.assertTrue(violations.size() > 0);
    }

    @Test
    public void whenPropertiesNotEmptyThenFailPasses() {
        MembershipCard membershipCard = new MembershipCard();
        membershipCard.setMembershipNumber("123456");
        membershipCard.setNameOnCard("JOHN SMITH");

        Set<ConstraintViolation<MembershipCard>> violations = validator.validate(membershipCard);
        Assertions.assertEquals(0, violations.size());
    }

}

  1. Run the test, and you get :
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running org.example.demo.ValidatorTest
< ... snipped ... >
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.196 s - in org.example.demo.ValidatorTest

  1. Now change the POM file so that it uses SpringBoot 2.6.7 :
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.7</version>
        <relativePath/>
    </parent>

  1. Run the test, and you get :
INFO] Running org.example.demo.ValidatorTest
<... snipped ...>
[ERROR] Tests run: 2, Failures: 0, Errors: 2, Skipped: 0, Time elapsed: 0.17 s <<< FAILURE! - in org.example.demo.ValidatorTest
[ERROR] whenPropertiesEmptyThenFailValidation  Time elapsed: 0.026 s  <<< ERROR!
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method MembershipCard#setNameOnCard(String) redefines the configuration of WithNameOnCard#setNameOnCard(String).
    at org.example.demo.ValidatorTest.whenPropertiesEmptyThenFailValidation(ValidatorTest.java:27)

[ERROR] whenPropertiesNotEmptyThenFailPasses  Time elapsed: 0.003 s  <<< ERROR!
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method MembershipCard#setNameOnCard(String) redefines the configuration of WithNameOnCard#setNameOnCard(String).
    at org.example.demo.ValidatorTest.whenPropertiesNotEmptyThenFailPasses(ValidatorTest.java:37)

[ERROR] Errors: 
[ERROR]   ValidatorTest.whenPropertiesEmptyThenFailValidation:27 » ConstraintDeclaration
[ERROR]   ValidatorTest.whenPropertiesNotEmptyThenFailPasses:37 » ConstraintDeclaration ...
[INFO] 
[ERROR] Tests run: 3, Failures: 0, Errors: 2, Skipped: 0


NOTE: Like I mentioned above, I understand the reason for the error so that the class and interface must obey the Liskov substitution principle.

The fixes for the error to go away are, either :

  • Removing the setter method setNameOnCard() from the WithNameOnCard interface. OR
  • Moving the validation annotations from the fields of the implementing class to the getter method of the interface. OR
  • Moving only the @NotNull annotation from the nameOnCard field of the MembershipCard class to the getNameOnCard() method of the NameOnCard interface, while leaving the @NotBlank annotation on the nameOncard field in the class. ( This last fix I actually don't understand. What's unique about @NotNull vs @NotBlank that would cause this difference between 2.6.6 and 2.6.7 ? )

What I don't understand why was this error NOT generated with SpringBoot 2.6.6 when the Hibernate validator artifact remained the same between 2.6.6 and 2.6.7.

So it must be something else that is causing this change in behaviour, but I can't pinpoint to what it is.

CodePudding user response:

Spring Boot 2.6.7 upgraded the Lombok version. The newer Lombok version propagates the field annotations to the generated setter methods. As a work around, you can downgrade the Lombok version by setting the Maven property:

<lombok.version>1.8.22</lombok.version>
  • Related