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.