Affects: spring-boot 2.6.7

Expected Behavior

I would expect that:

  • if I use a Kotlin data class as a request body with properties that cannot be null, properties mustn't be null at runtime,
  • if I use a Kotlin data class as a request body with properties that cannot be null and I send the payload with nulls, I get a Bad Request in the response.

Current Behaviour

I would expect that:

  • if I use a Kotlin data class as a request body with properties that cannot be null, properties can be null at runtime,
  • if I use a Kotlin data class as a request body with properties that cannot be null and I send the payload with nulls, sometimes I get a Bad Request and sometimes I get a Internal 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.