Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialization and Deserialization of Kotlin data class fails on PolymorphicTypeValidator with Any #819

Open
1 task done
effx13 opened this issue Jul 14, 2024 · 6 comments

Comments

@effx13
Copy link

effx13 commented Jul 14, 2024

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

When trying to deserialize class that contains kotlin value class, Jackson throws InvalidTypeIdException with missing type id property '@class' on ObjectMapper.DefaultTyping.NON_FINAL
Despite adding @JsonCreator annotation, I got the same result.

And trying to serialize above class with ObjectMapper.DefaultTyping.NON_FINAL, Jackson throws JsonMappingException with class ServerName cannot be cast to class java.lang.String (ServerName is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap') (through reference chain: TestDto["serverName"])

I'm using PolymorphicTypeValidator and Any class to do serialization and deserialization in Redis. But no matter what settings and annotations I use, the serialization and deserialization fails.

Version Information

JVM 21
Kotlin 1.8

jackson-core:2.17.2
jackson-databind:2.17.2
jackson-annotations:2.17.2
jackson-datatype-jsr310:2.17.2
jackson-module-kotlin:2.17.2

Reproduction

@JvmInline
value class ServerName(
  val value: String,
) {
  companion object {
    @JsonCreator
    @JvmStatic
    fun fromValue(value: String): ServerName {
      return ServerName(value)
    }
  }
}

data class TestDto(
  val id: Int,
  val serverName: ServerName
)

val objectMapper = ObjectMapper()
  .registerKotlinModule()
  .registerModule(JavaTimeModule())
  .activateDefaultTyping(
    BasicPolymorphicTypeValidator.builder().allowIfBaseType(Any::class.java).build(),
    ObjectMapper.DefaultTyping.NON_FINAL, // or EVERYTHING
    JsonTypeInfo.As.PROPERTY,
  )

// on NON_FINAL
fun main() {
  val serverName = ServerName("TEST")
  val testDto = TestDto(1, serverName)

  val serialized = objectMapper.writeValueAsBytes(testDto)
  println(String(serialized))
  // Expected output: {"id":1,"serverName":"TEST"}
  // Actual output: {"id":1,"serverName":{"value":"TEST"}}

  val deserialized = objectMapper.readValue(serialized, Any::class.java) // Because of RedisSerializer
  println(deserialized)
  // Expected output: TestDto(id=1, serverName=ServerName(value=TEST))
  // Actual output: Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class java.lang.Object]: missing type id property '@class'
}

// on EVERYTHING
fun main() {
  val serverName = ServerName("TEST")
  val testDto = TestDto(1, serverName)

  val serialized = objectMapper.writeValueAsBytes(testDto)
  println(String(serialized))
  // Expected output: {"id":1,"serverName":"TEST"}
  // Actual output: {"id":1,"serverName":{"value":"TEST"}}

  val deserialized = objectMapper.readValue(serialized, Any::class.java) // Because of RedisSerializer
  println(deserialized)
  // Expected output: TestDto(id=1, serverName=ServerName(value=TEST))
  // Actual output: Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: class ServerName cannot be cast to class java.lang.String (ServerName is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap') (through reference chain: TestDto["serverName"])
}

Expected behavior

No response

Additional context

No response

@effx13 effx13 added the to-evaluate Issue that has been received but not yet evaluated label Jul 14, 2024
@cowtowncoder
Copy link
Member

Kotlin issues belong under jackson-module-kotlin in general, will transfer.

@k163377
Copy link
Contributor

k163377 commented Oct 13, 2024

Created a PR because I found a problem with databind.
FasterXML/jackson-databind#4749

Will check again after this is merged.

@k163377 k163377 added enhancement and removed to-evaluate Issue that has been received but not yet evaluated labels Oct 14, 2024
@k163377
Copy link
Contributor

k163377 commented Oct 14, 2024

Checked.

This is a new feature addition and an implementation policy should be discussed.
I am not familiar with this feature and would like to hear any opinions on the implementation policy.


@effx13 has submitted “serverName”: “TEST” as an expectation, but I disagree with this for now.

kotlin-module treats properties defined in value class as if they were typed.
If so, shouldn't “serverName”:[“${type name}”, “TEST”] be the expected value?

As for the implementation, the base class of ValueClassUnboxSerializer will be changed to StdScalarSerializer.
The following is a tentative test for the prototype.

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import kotlin.test.Test

class GitHub819 {
    @JvmInline
    value class ServerName(val value: String) {
        companion object {
            @JsonCreator
            @JvmStatic
            fun fromValue(value: String): ServerName {
                return ServerName(value)
            }
        }
    }

    data class TestDto(
        val serverName: ServerName
    )

    // on EVERYTHING
    @Test
    fun everything() {
        val objectMapper = ObjectMapper()
            .registerKotlinModule()
            .registerModule(JavaTimeModule())
            .activateDefaultTyping(
                BasicPolymorphicTypeValidator.builder().allowIfBaseType(Any::class.java).build(),
                ObjectMapper.DefaultTyping.EVERYTHING,
                JsonTypeInfo.As.PROPERTY,
            )

        val serverName = ServerName("TEST")
        val testDto = TestDto(serverName)

        val serialized = objectMapper.writeValueAsString(testDto)
        // -> {"@class":"com.fasterxml.jackson.module.kotlin.test.github.GitHub819$TestDto","serverName":["com.fasterxml.jackson.module.kotlin.test.github.GitHub819$ServerName","TEST"]}
        println(serialized)

        val deserialized = objectMapper.readValue(serialized, Any::class.java) // Because of RedisSerializer
        // -> TestDto(serverName=ServerName(value=TEST))
        println(deserialized)
    }
}

As a side note, as far as the prototype is concerned, I feel that it would be difficult to implement in any other way.

@effx13
Copy link
Author

effx13 commented Oct 14, 2024

@k163377
Thank you for check my issue.

I agree, as you said, in BasicPolymorphicTypeValidator it should come out as “serverName”:[“${type name}”, “TEST”].
so is it correct that this feature is not implemented yet? I tried various methods, but I was unable to serialization and deserialization on BasicPolymorphicTypeValidator.

@k163377
Copy link
Contributor

k163377 commented Oct 14, 2024

I agree, as you said, in BasicPolymorphicTypeValidator it should come out as “serverName”:[“${type name}”, “TEST”].

👍

so is it correct that this feature is not implemented yet?

First, a version that incorporates the fix for the bug is required(2.18.1 or later).

After that, you may be able to solve your use case by setting up a custom serializer based on StdScalarSerializer.

As for the kotlin-module, the changes could be small, but require a lot of testing.

@cowtowncoder
Copy link
Member

Quick note: 2.18.1 not yet released; will be released in near future (2-3 weeks), but need to combine with other fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants