micronaut-serialization icon indicating copy to clipboard operation
micronaut-serialization copied to clipboard

Data Binding using Kotlin data class will ignore constructor defaults

Open davidsonnabend opened this issue 1 year ago • 2 comments

Expected Behavior

It should be possible to bind JSON to a Kotlin data class with default value(s) in the constructor without providing values (in the JSON) for the already defined constructor defaults.

Having this data class:

@Serdeable
data class Foo(
    val bar: String,
    var validFrom: LocalDate = LocalDate.now(),
)

And the following controller:

@Controller("/foos")
class FooController {
    @Post
    fun save(@Body foo: Foo): HttpResponse<Foo> {
        return HttpResponse.created(foo)
    }
}

This JSON should be valid to create an instance of Foo when posting to /foos:

{
    "bar": "some_value"
}

Actual Behaviour

When trying to bind JSON (see example above) to a Kotlin data class containing a default value in the constructor, I will get the following error when posting a JSON without an explicit value for the non-nullable (here validFrom) property:

"Failed to convert argument [foo] for value [null] due to: Unable to deserialize type [com.example.Foo]. Required constructor parameter [LocalDate validFrom] at index [1] is not present or is null in the supplied data"

Steps To Reproduce

  1. Clone example application
  2. Run test using ./gradlew test
  3. Test will fail caused by a Bad Request

Or

  1. Clone example application
  2. Run application using ./gradlew run
  3. Post a JSON with validFrom value will return a JSON representation of the Foo instance:
curl --location 'http://localhost:8080/foos' \
--header 'Content-Type: application/json' \
--data '{
    "bar": "some_value",
    "validFrom": "2023-11-10"
}'
// Response
{
    "bar": "some_value",
    "validFrom": "2023-11-10"
}
  1. Post a JSON without validFrom value will return a 400 Bad Request:
curl --location 'http://localhost:8080/foos' \
--header 'Content-Type: application/json' \
--data '{
    "bar": "some_value"
}'
// Response
{
    "_links": {
        "self": [
            {
                "href": "/foos",
                "templated": false
            }
        ]
    },
    "_embedded": {
        "errors": [
            {
                "message": "Failed to convert argument [foo] for value [null] due to: Unable to deserialize type [com.example.Foo]. Required constructor parameter [LocalDate validFrom] at index [1] is not present or is null in the supplied data",
                "path": "/foo"
            }
        ]
    },
    "message": "Bad Request"
}

Environment Information

  • Operating System: Ubuntu 23.10
  • JDK Version: 17.0.8.1-zulu

Example Application

https://github.com/davidsonnabend/micronaut-serialization-kotlin-constructor-defaults-deserialization

Version

4.1.6

davidsonnabend avatar Nov 13 '23 13:11 davidsonnabend

Try declare the Foo class to this:

@Serdeable
data class Foo(
    val bar: String,
    var validFrom: LocalDate? = LocalDate.now(),
)

yazidIT avatar Dec 29 '23 03:12 yazidIT

Try declare the Foo class to this:

@Serdeable
data class Foo(
    val bar: String,
    var validFrom: LocalDate? = LocalDate.now(),
)

This is a valid workaround but it would be nice if this will also work with non-nullable types.

davidsonnabend avatar Jan 08 '24 08:01 davidsonnabend