3

Background

I have a data class

@Data
public class Data {
    @lombok.NonNull
    private String name;
}

a controller

@MessageMapping("/data")
public void handleData(@Validated Data data) throws Exception {
    if (data.getName().compareTo("Alice") == 0) {
        logger.info("Alice is here!");
    }
}

and a bean to config jackson to convert booleans to integers (True -> 1, False -> 0)

@Bean
ObjectMapper registerObjectMapper() {
    ObjectMapper mapper = new ObjectMapper();

    SimpleModule module = new SimpleModule("MyBoolSerializer");
    module.addSerializer(Boolean.class, new MyBoolSerializer());
    module.addDeserializer(Boolean.class, new MyBoolDeserializer());
    module.addSerializer(boolean.class, new MyBoolSerializer());
    module.addDeserializer(boolean.class, new MyBoolDeserializer());
    
    mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

    return mapper;
}

When I make a request to /data without setting the name parameter, Jackson will set it to null. However, I get the following exception (unwrapped)

org.springframework.messaging.converter.MessageConversionException: 
Could not read JSON: Cannot construct instance of `com.example.myapp.entity.Data`
(no Creators, like default constructor, exist): 
cannot deserialize from Object value (no delegate- or property-based Creator)

Attempted fix

So then I added @NoArgsConstructor to Data.

@Data
@NoArgsConstructor   // <<<<
public class Data {
    @lombok.NonNull
    private String name;
}

Now request to /data will result in NullPointerException. The parameters are NOT null-checked and the if-statement is run.

I tried to use hibernate-validator's @NotNull annotation to the name attribute in Data, but the result is the same: NPE.

Question

What I thought about the @NonNull and @NotNull annotations is that they help validating the data so that I don't need to manually validate them in controllers(check null, check within range, etc.). However it seems to be only valid if the default constructor does not exist. It makes sense because null-checks are not performed in default constructor (no data to validate...).

But then it contradicts with the exception I encountered.

Info that might help

I have amqp enabled and it has its own MessageConverter bean that returns a new Jackson2JsonMessageConverter instance.

import org.springframework.amqp.support.converter.MessageConverter;
@Bean
MessageConverter jsonMessageConverter() {
    return new Jackson2JsonMessageConverter();
}

Any thoughts?

BTW, the title might be a bit ambiguous or misleading, but I genuinely have no idea what the problem is.

--- Edit 1: pom.xml

<?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.7.7</version>
        <relativePath />
        <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo2</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>18</java.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>3.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>8.0.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.4.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-reactor-netty</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</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>

</project>
Henry Fung
  • 380
  • 3
  • 12
  • The lombok `Nonnull` has a totally different approach/meaning then the validation `NotNull`. Also you are using `@MessageMapping` so this implies a websocket connection (if it is a controller). You would need `@Payload` and `@Validated` for validation with JSR303 annotations to work. You seem to be lacking the first one. And I would strongly suggest to use the validation one and not the lombok one. – M. Deinum Feb 02 '23 at 09:58
  • @M.Deinum I just took away the lombok `@Nonnull` and replaced with validation `@NotNull`. There used to be the `@Payload` but with or without it, the validation is still not performed... – Henry Fung Feb 02 '23 at 10:17
  • Which annotations are you using, do you have `spring-boot-starter-validation` on your dependencies? Or just `validation-api` (if so replace with the starter). Also which annotation you are using and which version of Spring Boot are you using? Please add your pom.xml or build.gradle. – M. Deinum Feb 02 '23 at 10:52
  • @M.Deinum Just added `pom.xml` to the question. I have `spring-boot-starter-validation`. – Henry Fung Feb 03 '23 at 00:49
  • 1
    Remove the `hibernate-validator` dependency and the version of the `spring-boot-starter-validation`. You are now mixing SPring Boot 2.7.7 and Spring Boot 3.0.2 modules which aren't compatible (never mix modules from different versions of a framework). The `spring-boot-starter-validation` will include the `hibernate-validator` which is compatible with your version of Spring (Boot). hibernate validator 8 isn't compatible. Spring Boot 2.7 is for JavaEE and Spring Boot 3.0 is JakartaEE (different namespaces). Hibernate validator 8 is JakartaEE. – M. Deinum Feb 03 '23 at 07:58

2 Answers2

1

The problem is your pom.xml, you are mixing modules from different versions of Spring Boot, which is not going to work.

Spring Boot 3.0 and higher are for JakartaEE while Spring Boot 2.7 and lower are for JavaEE. The Hibernate Validator version you are including is for JakartaEE which isn't compatible with Spring Framework 5 (used by Spring Boot 2.7). Hence no validation will take place simply because it doesn't understand the annotations.

To fix remove the version from the spring-boot-starter-validation and remove the hibernate-validator dependency. Everything with compatible versions will be pulled in by spring-boot-starter-validation.

<?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.7.7</version>
        <relativePath />
        <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo2</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>18</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-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-reactor-netty</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</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>

</project>

TIP: Instead of using spring-rabbit as a single dependency use spring-boot-starter-amqp which will pull in all you need.

When changing the dependencies you probably get compilation errors on the validation annotations. Change the package from jakarta.validation to javax.validation to import the proper validation annotations.

M. Deinum
  • 115,695
  • 22
  • 220
  • 224
  • The version tag of `spring-boot-starter-validation` is added automatically when adding it via VSCode's `Maven for Java` extension. Removing the version tag (and `hibernate-validator`) solved the problem. Thanks! – Henry Fung Feb 03 '23 at 08:54
0

I guess you are looking for @NotEmpty or @NotNull in jakarta-validation api. Kindly consider the sample below.

import lombok.Data;
import javax.validation.constraints.NotEmpty;

@Data
public class Data {
    
    @NotEmpty(message="Name cannot be null or blank")
    private String name;
}

Validations are present in

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
      <version>Your spring boot version</version>
</dependency>
Anurag Ambuj
  • 101
  • 1
  • 7
  • I just tried adding `@NotNull` and `@NotEmpty` to the `name` property (w/ `spring-boot-starter-validation` package). But the problem remains... – Henry Fung Feb 03 '23 at 01:04
  • As per M. Deinum's answer, it seems that you provided an incorrect spring boot version. Kindly mark this answer as valid, as it is correct and it might help someone in future who stumbles upon this. – Anurag Ambuj Feb 14 '23 at 07:29