micronaut-projects / micronaut-serialization

Build Time Serialization APIs for Micronaut
Apache License 2.0
26 stars 18 forks source link

Non-nullable Long Kotlin data class field initialized with 0 when field is missing in payload #880

Closed erkieh closed 4 months ago

erkieh commented 5 months ago

Expected Behavior

Posting validated HTTP JSON Payloads with missing values for Non-nullable Long Kotlin data class fields should result in a 400 bad request response,

Actual Behaviour

Non-nullable Long Kotlin data class fields are initialized with 0 when the field is missing in the payload. Strings for example behave differently. They don't get defaulted. Instead the result will be an 500 Internal server error with a NullPointerException happening on the server side.

Steps To Reproduce

Run tests in sample app

Environment Information

Windows 10 Java 21

Example Application

git@github.com:erkieh/demonullhandling.git

Version

4.5.0-SNAPSHOT

roar-skinderviken commented 4 months ago

This issue seems to be related to this issue except for that

Please note that any default values in Kotlin data classes are ignored during deserialization in this issue.

A possible workaround for returning 400 bad request when Long-value is missing in JSON, may be to use @Min along with @Bindable annotations (given that only non-negative numbers are valid):

import io.micronaut.core.bind.annotation.Bindable
import io.micronaut.serde.annotation.Serdeable
import jakarta.validation.constraints.Min

@Serdeable
data class NonNullDto(
    @Min(0L)
    @Bindable(defaultValue = "-1")
    val longField: Long
)

Tests

import com.deserializenull.rest.dto.NonNullDto
import com.deserializenull.rest.dto.NullDto
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest

@MicronautTest
class NonNullDtoControllerTest(
    @Client("/") httpClient: HttpClient
) : StringSpec({

    "test deserialization with null data, expect 400 response" {
        val exception = shouldThrow<HttpClientResponseException> {
            httpClient.toBlocking().exchange(HttpRequest.POST("/nonnulldto", NullDto()), NullDto::class.java)
        }

        exception.status.code shouldBe 400
    }

    "test deserialization with valid data, expect 200 response" {
        val response = httpClient.toBlocking().exchange(HttpRequest.POST("/nonnulldto", NonNullDto(longField = 1)), NonNullDto::class.java)
        val responseBody = response.getBody(NonNullDto::class.java).get()

        response.status.code shouldBe 200
        responseBody.longField shouldBe 1
    }
})
dstepanov commented 4 months ago

Please create / update the sample app with your use case

dstepanov commented 4 months ago

The behaviour is the same as a default in Jackson. The fix is a configuration to fail on null primitives.