Home > Mobile >  Spring Boot Data class not respecting @NonNull or @NotNull if default constructor exists
Spring Boot Data class not respecting @NonNull or @NotNull if default constructor exists

Time:02-03

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>

CodePudding user response:

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>

CodePudding user response:

The issue you are encountering is because the AMQP's MessageConverter bean is using a new instance of Jackson2JsonMessageConverter to deserialize the JSON into your Data class, but it is not using the custom ObjectMapper you registered in the registerObjectMapper bean method.

To resolve this, you need to configure your custom ObjectMapper in the jsonMessageConverter bean method, so it's used when deserializing the JSON into your Data class:

@Bean
MessageConverter jsonMessageConverter() {
    Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter();
    messageConverter.setObjectMapper(registerObjectMapper());
    return messageConverter;
}

This way, the custom ObjectMapper will be used for deserialization, and the Boolean values will be properly converted to 1 and 0. Also, the @NonNull and @NotNull annotations will be honored during deserialization, and any missing name attribute in the JSON will cause an exception to be thrown before the handleData method is even called, avoiding the NPE.

  • Related