Affects: spring-boot 2.6.7
Expected Behavior
I would expect that:
- if I use a
Kotlin data class
as arequest body
with properties that cannot benull
, properties mustn't benull
at runtime, - if I use a
Kotlin data class
as arequest body
with properties that cannot benull
and I send thepayload
withnulls
, I get aBad Request
in the response.
Current Behaviour
I would expect that:
- if I use a
Kotlin data class
as arequest body
with properties that cannot benull
, properties can benull
at runtime, - if I use a
Kotlin data class
as arequest body
with properties that cannot benull
and I send thepayload
withnulls
, sometimes I get aBad Request
and sometimes I get aInternal Server Error
in the response.
Request ends up with an expected Bad Request (400)
Minimal reproducible code (the gist of this issue):
package io.github.ryszardmakuch.springbootkotlinissue
import org.apache.logging.log4j.LogManager
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
// expected behavior because I cannot instantiate ExampleRequest(element = null)
data class ExampleRequest(val element: String)
@RestController
class ExampleController {
@PostMapping("/example")
fun examplePostRequest(@RequestBody exampleRequest: ExampleRequest): ResponseEntity<Void> {
logger.info("Element's length: ${exampleRequest.element.length}")
return ResponseEntity.ok().build()
}
companion object {
private val logger = LogManager.getLogger(ExampleController::class.java)
}
}
Request:
curl -X POST localhost:8080/example
-H 'Content-Type: application/json'
-d '{"element": null}'
Response:
{"timestamp":"2022-05-02T19:31:35.715+00:00","status":400,"error":"Bad Request","path":"/example"}
In logs I can see:
2022-05-02 21:33:01.139 WARN 12654 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error:
Instantiation of [simple type, class io.github.ryszardmakuch.springbootkotlinissue.ExampleRequest] value failed for JSON property element due to missing (therefore NULL) value for creator parameter element which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException:
Instantiation of [simple type, class io.github.ryszardmakuch.springbootkotlinissue.ExampleRequest] value failed for JSON property element due to missing (therefore NULL) value for creator parameter element which is a non-nullable type<EOL> at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 17] (through reference chain: io.github.ryszardmakuch.springbootkotlinissue.ExampleRequest["element"])]
Request ends up with an unexpected Internal Server Error (500) due to an unexpected NullPointerException
Minimal reproducible code (the gist of this issue):
package io.github.ryszardmakuch.springbootkotlinissue
import org.apache.logging.log4j.LogManager
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
// unexpected behavior because I cannot instantiate ExampleRequest(elements = listOf(null))
data class ExampleRequest(val elements: List<ExampleElement>)
data class ExampleElement(val value: String)
@RestController
class ExampleController {
@PostMapping("/example")
fun examplePostRequest(@RequestBody exampleRequest: ExampleRequest): ResponseEntity<Void> {
exampleRequest.elements.map {
logger.info("Element's length: ${it.value.length}") // throws unexpected java.lang.NullPointerException
}
return ResponseEntity.ok().build()
}
companion object {
private val logger = LogManager.getLogger(ExampleController::class.java)
}
}
Request:
curl -X POST localhost:8080/example
-H 'Content-Type: application/json'
-d '{"elements": [ null ]}'
Response:
{"timestamp":"2022-05-02T19:39:02.284+00:00","status":500,"error":"Internal Server Error","path":"/example"}
In logs I can see:
java.lang.NullPointerException: null
at io.github.ryszardmakuch.springbootkotlinissue.ExampleController.examplePostRequest(ExampleController.kt:17) ~[main/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
(...)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743) ~[tomcat-embed-core-9.0.62.jar:9.0.62]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.62.jar:9.0.62]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.62.jar:9.0.62]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.62.jar:9.0.62]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.62.jar:9.0.62]
at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]
I would expect com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
due to non-nullable ExampleElement
type and a Bad Request
response in the result.
Typically a NullPointerException
may happen when I declared elements
as List<ExampleElement?>
and unsafely called the value
like in the example below:
data class ExampleRequest(val elements: List<ExampleElement?>)
// ...
logger.info("Element's length: ${it!!.value.length}")
build.gradle.kts
:
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.6.7"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.21"
kotlin("plugin.spring") version "1.6.21"
}
group = "io.github.ryszardmakuch"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
Comment From: ryszardmakuch
Closing this issue.
I've missed the StrictNullChecks
feature which could be enabled in KotlinModule
. It prevents against problem I've described 🤦🏻
Instantiation of kotlin.String collection failed for JSON property list due to null value in a collection that does not allow null values
at [Source: (String)"{ "elements": [ null ] }"; line: 1, column: 20] (through reference chain: io.github.ryszardmakuch.springbootkotlinissue.ExampleRequest["elements"])
Moreover, it seems that I should have opened this issue as the question in the jackson-module-kotlin project due to the fact the described problem is unrelated to Spring
itself.