kotlinjackson

How to make JsonUwrapped annotation work in Kotlin data class


How do to make Jackson @JsonUnwrapped annotation work properly in Kotlin data class for deserialisation?

Other than creation of a custom deserialiser for this class and adding it to object mapper as a module, I did not find any simple solution.

Here is my code example:

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
@JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy::class)
@Schema(description = "Customer Details", allOf = [Address::class, Person::class])
data class CustomerDetails(
  @JsonUnwrapped val addressDetails: Address?,// address related fields
  @JsonUnwrapped val personDetails: Person? // person related fields
)

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
@JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy::class)
data class Address(
  val houseNumber: Int?,
  val streetName: String?,
  val city: String?
)

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
@JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy::class)
data class Person(
  val firstName: String?,
  val lastName: String?,
  val initials: String?
)

Because of the @Schema annotation, the swagger page works fine. Because of @JsonUnwrapped annotation, the serialisation of object to JSON works as intended. But the deserialisation fails with the error: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot define Creator property "property name" as @JsonUnwrapped: combination not yet supported


Solution

  • The combination of @JsonCreator with @JsonUnwrapped (which is said to be "not yet supported" in the exception message) will be supported in version 2.19. See this merged PR. I believe your code will work as is, if you use that version when it is released.

    For now, I have found that adding a parameterless @JsonCreator factory method works:

    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
    @JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy::class)
    data class CustomerDetails(
        @field:JsonUnwrapped
        val addressDetails: Address?,
        @field:JsonUnwrapped
        val personDetails: Person?
    ) {
        companion object {
            @JsonCreator
            @JvmStatic
            private fun make() = CustomerDetails(null, null)
        }
    }
    

    Jackson will call make and set the fields correctly.

    Working example (using jackson-module-kotlin):

    val json = "{\"houseNumber\":1,\"streetName\":\"a\",\"city\":\"b\",\"firstName\":\"c\",\"lastName\":\"d\",\"initials\":\"e\"}"
    jacksonObjectMapper().readValue<CustomerDetails>(json).also { println(it) }
    

    If you don't use jackson-module-kotlin, you can replace make with a parameterless constructor instead (the Kotlin module doesn't call the parametersless constructor for some reason, which is why I had to use a factory method).